Using .NET with Caché eXTreme
Using eXTreme Event Persistence
[Back] [Next]
   
Server:docs2
Instance:LATEST
User:UnknownUser
 
-
Go to:
Search:    

Caché eXTreme Event Persistence (XEP) provides extremely rapid storage and retrieval of .NET structured data. It implements a fast, efficient object-to-global mapping algorithm that harnesses the power of the Globals API (see Using the Globals API), but no knowledge of the Globals API is required. XEP provides ways to control schema generation for optimal mapping of complex data structures, but schemas for simpler data structures can often be generated and used without modification. Unlike the Globals API, XEP can use either in-process communications or a TCP/IP connection.

The following topics are discussed in this chapter:
Introduction to Event Persistence
A persistent event is a Caché database object that stores a persistent copy of the data fields in a .NET object. By default, the eXTreme Event Persistence API stores each event as regular %Persistent object. Storage is automatically configured so that the data will be accessible to Caché by other means, such as objects, SQL, or direct global access.
Before a persistent event can be created and stored, XEP must analyze the corresponding .NET class and import a schema, which defines how the data structure of a .NET object is projected to a Caché persistent event. A schema can use either of the following two object projection models:
See Schema Import Models for a detailed discussion of both models.
Once a schema has been imported, XEP can be used to store, query, update and delete data at very high rates. Stored events are immediately available for querying, or for full object or global access. The EventPersister, Event, and EventQuery<> classes provide the main features of the XEP API. They are used in the following sequence:
The next section (see Simple Applications to Store and Query Persistent Events) describes two very short applications that demonstrate all of these features.
When importing a schema, XEP acquires basic information by analyzing the .NET class. You can supply additional information that allows XEP to generate indexes and override the default rules for importing fields (see Schema Customization).
Fields of a persistent event can be simple numeric types and their associated System types, strings, objects (projected as embedded/serial objects), enumerations, and types derived from collection classes. These types can also be contained in arrays, nested collections, and collections of arrays. See Schema Mapping Rules for detailed information.
Simple Applications to Store and Query Persistent Events
This section describes two very simple applications that use XEP to create and access persistent events:
Note:
It is assumed that these applications have exclusive use of the system, and run in two consecutive processes.
Both programs use instances of xep.samples.SingleStringSample, which is one of the classes defined in the XEP sample programs (see XEP Samples for details about the sample programs).
The StoreEvents Program
In StoreEvents, a new instance of EventPersister is created and connected to a specific namespace on the Caché server. A schema is imported for the SingleStringSample class, and the test database is initialized by deleting all existing events from the extent of the class. An instance of Event is created and used to store an array of SingleStringSample objects as persistent events. The connection is then terminated. The new events will persist in the Caché database, and will be accessed by the QueryEvents program (described in the next section).
The StoreEvents Program: Creating a schema and storing events
using System;
using InterSystems.XEP;
using xep.samples; // compiled XEPTest.csproj

public class StoreEvents {
  private static String className = "xep.samples.SingleStringSample";
  private static SingleStringSample[] eventData = SingleStringSample.generateSampleData(12);

  public static void Main(String[] args) {
    for (int i=0; i < eventData.Length; i++) {
      eventData[i].name = "String event " + i;
    }
    try {
      Console.WriteLine("Connecting and importing schema for " + className);
      EventPersister myPersister = PersisterFactory.CreatePersister();
      myPersister.Connect("User", "_SYSTEM", "SYS");
      try { // delete any existing SingleStringSample events, then import new ones
        myPersister.DeleteExtent(className); 
        myPersister.ImportSchema(className); 
      }
      catch (XEPException e) { Console.WriteLine("import failed:\n" + e); }
      Event newEvent = myPersister.GetEvent(className);
      long[] itemIDs = newEvent.Store(eventData);  // store array of events
      Console.WriteLine("Stored " + itemIDs.Length + " of " 
        + eventData.Length + " objects. Closing connection...");
      newEvent.Close();
      myPersister.Close();
    }
    catch (XEPException e) { Console.WriteLine("Event storage failed:\n" + e); }
  } // end Main()
} // end class StoreEvents
Before StoreEvents.Main() is called, the xep.samples.SingleStringSample.generateSampleData() method is called to generate sample data array eventData (see XEP Samples for information on sample classes).
In this example, XEP methods perform the following actions:
All of these methods are discussed in detail later in this chapter. See Creating and Connecting an EventPersister for information on opening, testing, and closing an eXTreme connection. See Importing a Schema for details about schema creation. See Storing and Modifying Events for details about using the Event class and deleting an extent.
The QueryEvents Program
This example assumes that QueryEvents runs immediately after the StoreEvents process terminates (see The StoreEvents Program). QueryEvents establishes a new database connection that accesses the same namespace as StoreEvents. An instance of EventQuery<> is created to iterate through the previously stored events, print their data, and delete them.
The QueryEvents Program: Fetching and processing persistent events
using System;
using InterSystems.XEP;
using SingleStringSample = xep.samples.SingleStringSample; // compiled XEPTest.csproj

public class QueryEvents {
  public static void Main(String[] args) {
    EventPersister myPersister = null;
    EventQuery<SingleStringSample> myQuery = null;
    try {
// Open a connection, then set up and execute an SQL query
      Console.WriteLine("Connecting to query SingleStringSample events");
      myPersister = PersisterFactory.CreatePersister();
      myPersister.Connect("User","_SYSTEM","SYS");
      try {
        Event newEvent = myPersister.GetEvent("xep.samples.SingleStringSample");
        String sql = "SELECT * FROM xep_samples.SingleStringSample WHERE %ID BETWEEN 3 AND ?";
        myQuery = newEvent.CreateQuery<SingleStringSample>(sql);
        newEvent.Close();
        myQuery.AddParameter(12);  // assign value 12 to SQL parameter
        myQuery.Execute();
      }
      catch (XEPException e) {Console.WriteLine("createQuery failed:\n" + e);}

// Iterate through the returned data set, printing and deleting each event
      SingleStringSample currentEvent;
      currentEvent = myQuery.GetNext(); // get first item
      while (currentEvent != null) {
        Console.WriteLine("Retrieved " + currentEvent.name);
        myQuery.DeleteCurrent();
        currentEvent = myQuery.GetNext(); // get next item
      }
      myQuery.Close();
      myPersister.Close();
    }
    catch (XEPException e) {Console.WriteLine("QueryEvents failed:\n" + e);}
  } // end Main()
}  // end class QueryEvents
In this example, XEP methods perform the following actions:
All of these methods are discussed in detail later in this chapter. See Creating and Connecting an EventPersister for information on opening, testing, and closing an eXTreme connection. See Using Queries for details about creating and using an instance of EventQuery<>.
Creating and Connecting an EventPersister
The EventPersister class is the main entry point for the XEP API. It provides the methods for connecting to the database, importing schemas, handling transactions, and creating instances of Event to access events in the database.
An instance of EventPersister is created and destroyed by the following methods:
The following methods are used to create a connection:
It is important to understand that only one eXTreme connection can exist in a process, and all connected objects in the process will reference that connection instance. For example, an Xep.EventPersister object and a Globals.Connection object in the same process would share the same underlying connection.
The following example establishes an in-process eXTreme connection:
Creating and Connecting an EventPersister: Creating an eXTreme connection
// Open an eXTreme connection
  String namespc = "USER";
  String username = "_SYSTEM";
  String password = "SYS";
  EventPersister myPersister = PersisterFactory.CreatePersister();
  myPersister.Connect(namespc, username, password);
  // perform event processing here . . .
  myPersister.Close();
The PersisterFactory.CreatePersister() method creates a new instance of EventPersister. Only one instance is required in a process.
The EventPersister.Connect() method establishes an in-process eXTreme connection If no connection exists in the current process, a new eXTreme connection is created. If a connection already exists, the method returns a reference to the existing connection object.
When the application is ready to exit, the EventPersister.Close() method must always be called to release resources used by the underlying native code.
Important:
Always call Close() to avoid memory leaks
It is important to always call Close() on an instance of EventPersister before it goes out of scope. Failing to close it can cause serious memory leaks because .NET garbage collection cannot release resources allocated by the underlying native code.
Establishing a TCP/IP Connection
When an XEP application requires access to a database on another machine, it may be desirable to establish a standard TCP/IP connection rather than an in-process eXTreme connection. Although the TCP/IP connection is somewhat slower, the difference depends on the complexity of the events that are being stored. For extremely simple events with only one or two fields, the eXTreme connection is much faster. For more complex events, the difference is much less pronounced, and may be negligible for very complex events.
A TCP/IP connection is established when the call to Connect() specifies the optional host and port arguments, as demonstrated in the following example:
Creating and Connecting an EventPersister: Creating a TCP/IP connection
// Open a TCP/IP connection
  string host = "127.0.0.1";
  int port = 1972;
  myPersister.Connect(host, port, "User", "_SYSTEM", "SYS");
  // perform event processing here . . .
  myPersister.Close();
The EventPersister.Connect() method is called with host and port arguments, followed by the same namespace, username, and password arguments as in the previous example (hard-coded in this example). This establishes a TCP/IP connection to the specified port of the specified host machine.
Accessing the Underlying Connections
XEP is a layer over the Globals API, and uses the same underlying connections. The following methods return the underlying eXTreme and DbConnection connections:
The eXTreme Connection object can be useful in XEP applications that also use the other APIs described in this book, since the same underlying connection is used by all of them. See Creating a Connection in the Globals API chapter for a more detailed description of eXTreme connections.
Importing a Schema
Before an instance of a .NET class can be stored as a persistent event, a schema must be imported for the class. The schema defines the database structure in which the event will be stored. XEP provides two different schema import models: flat schema and full schema. The main difference between these models is the way in which .NET objects are projected to Caché events. A flat schema is the optimal choice if performance is essential and the event schema is fairly simple. A full schema offers a richer set of features for more complex schemas, but may have an impact on performance. See Schema Mapping and Customization for a detailed discussion of schema models and related subjects.
The following methods are used to analyze a .NET class and import a schema of the desired type:
The import methods are identical except for the schema model used. The following example imports a simple test class and its dependent class:
Importing a Schema: Importing a class and its dependencies
The following classes are to be imported:
namespace test {
  public class MainClass {
    public MainClass() {}
    public String  myString;
    public test.Address  myAddress;
  }

  public class Address {
    public String  street;
    public String  city;
    public String  state;
  }
}
The following code uses ImportSchema() to import the main class, test.MainClass, after calling IsEvent() to make sure it can be projected. Dependent class test.Address is also imported automatically when test.MainClass is imported:
  try {
    Event.IsEvent("test.MainClass"); // throw an exception if class is not projectable
    myPersister.ImportSchema("test.MainClass");
  }
  catch (XEPException e) {Console.WriteLine("Import failed:\n" + e);}
In this example, instances of dependent class test.Address will be serialized and embedded in the same Caché object as other fields of test.MainClass. If ImportSchemaFull() had been used instead, stored instances of test.MainClass would contain references to instances of test.Address stored in a separate Caché class extent.
Storing and Modifying Events
Once the schema for a class has been imported (see Importing a Schema), an instance of Event can be created to store and access events of that class. The Event class provides methods to store, update, or delete persistent events, create queries on the class extent, and control index updating. This section discusses the following topics:
Creating and Storing Events
Instances of the Event class are created and destroyed by the following methods:
The following Event method stores .NET objects of the target class as persistent events:
The following example creates an instance of Event with SingleStringSample as the target class, and uses it to project an array of .NET SingleStringSample objects as persistent events. The example assumes that myPersister has already been created and connected, and that a schema has been imported for the SingleStringSample class. See Simple Applications to Store and Query Persistent Events for an example of how this is done. See XEP Samples for information on SingleStringSample and the sample programs that define and use it.
Storing and Modifying Events: Storing an array of objects
  SingleStringSample[] eventItems = SingleStringSample.generateSampleData(12);
  try {
    Event newEvent = myPersister.GetEvent("xep.samples.SingleStringSample");
    long[] itemIdList = newEvent.Store(eventItems);  // store all events
    int itemCount = 0;
    for (int i=0; i < itemIdList.Length; i++) {
      if (itemIdList[i]>0) itemCount++;
    }
    Console.WriteLine("Stored " + itemCount + " of " + eventItems.Length + " events");
    newEvent.Close();
  }
  catch (XEPException e) { Console.WriteLine("Event storage failed:\n" + e); }
Important:
Always call Close() to avoid memory leaks
It is important to always call Close() on instances of Event before they go out of scope or are reused. Failing to close them can cause serious memory leaks because .NET garbage collection cannot release resources allocated by the underlying native code.
Accessing Stored Events
Once a persistent event has been stored, an Event instance of that target class provides the following methods for reading, updating, deleting the event:
If the target class uses a standard object ID, it is specified as a long value (as returned by the Store() method described in the previous section). If the target class uses an IdKey, it is specified as an array of Object where each item in the array is a value for one of the fields that make up the IdKey (see Using IdKeys).
In the following example, array itemIdList contains a list of object ID values for some previously stored SingleStringSample events. The example arbitrarily updates the first six persistent events in the list and deletes the rest.
Note:
See Creating and Storing Events for the example that created the itemIdList array. This example also assumes that an EventPersister instance named myPersister has already been created and connected to the database.
Storing and Modifying Events: Fetching, updating, and deleting events
  // itemIdList is a previously created array of SingleStringSample event IDs
  try {
    Event newEvent = myPersister.GetEvent("xep.samples.SingleStringSample");
    int itemCount = 0;
    for (int i=0; i < itemIdList.Length; i++) {
      try { // arbitrarily update events for first 6 Ids and delete the rest
        SingleStringSample eventObject = (SingleStringSample)newEvent.GetObject(itemIdList[i]);
        if (i<6) {
          eventObject.name = eventObject.name + " (id=" + itemIdList[i] + ")" + " updated!";
          newEvent.UpdateObject(itemIdList[i], eventObject);
          itemCount++;
        } else {
          newEvent.DeleteObject(itemIdList[i]); 
        }
      }
      catch (XEPException e) {Console.WriteLine("Failed to process event:\n" + e);}
    }
    Console.WriteLine("Updated " + itemCount + " of " + itemIdList.Length + " events");
    newEvent.Close();
  }
  catch (XEPException e) {Console.WriteLine("Event processing failed:\n" + e);}
See XEP Samples for information on the sample programs that define and use the SingleStringSample class.
See Using Queries for a description of how to access and modify persistent events fetched by a simple SQL query.
Deleting Test Data
When initializing a test database, it is frequently convenient to delete an entire class, or delete all events in a specified extent. The following EventPersister methods delete classes and extents from the Caché database:
These methods are intended primarily for testing, and should be avoided in production code. See Classes and Extents in the Caché Programming Orientation Guide for a detailed definition of these terms.
Controlling Index Updating
By default, indexes are not updated when a call is made to one of the Event methods that act on an event in the database (see Accessing Stored Events). Indexes are updated asynchronously, and updating is only performed after all transactions have been completed and the Event instance is closed. No uniqueness check is performed for unique indexes.
Note:
This section only applies to classes that use standard object IDs or generated IdKeys (see Using IdKeys). Classes with user-assigned IdKeys can only be updated synchronously.
There are a number of ways to change this default indexing behavior. When an Event instance is created by EventPersister.GetEvent() (see Creating and Storing Events), the optional indexMode parameter can be set to specify a default indexing behavior. The following options are available:
The following Event methods can be used to control asynchronous index updating for the extent of the target class:
Using Queries
The Event class provides a way to create an instance of EventQuery<>, which can execute a limited SQL query on the extent of the target class. EventQuery<> methods are used to execute the SQL query, and to retrieve, update, or delete individual items in the query resultset.
The following topics are discussed:
Note:
The examples in this section assume that EventPersister object myPersister has already been created and connected, and that a schema has been imported for the SingleStringSample class. See Simple Applications to Store and Query Persistent Events for an example of how this is done.
Creating and Executing a Query
The following methods create and destroy an instance of EventQuery<>:
Queries submitted by an instance of EventQuery<E> will return .NET objects of the specified generic type E (the target class of the Event instance that created the query object). Queries supported by the EventQuery<> class are a subset of SQL select statements, as follows:
The following EventQuery<> methods define and execute the query:
The following example executes a simple query on events in the xep.samples.SingleStringSample extent (see XEP Samples for information on the sample programs that define and use the SingleStringSample class.).
Using Queries: Creating and executing a query
  Event newEvent = myPersister.GetEvent("xep.samples.SingleStringSample");
  EventQuery<SingleStringSample> myQuery = null;
  String sql = 
    "SELECT * FROM xep_samples.SingleStringSample WHERE %ID BETWEEN ? AND ?";

  myQuery = newEvent.CreateQuery<SingleStringSample>(sql);
  myQuery.AddParameter(3);  // assign value 3 to first SQL parameter
  myQuery.AddParameter(12);  // assign value 12 to second SQL parameter
  myQuery.Execute();   // get resultset for IDs between 3 and 12
The EventPersister.GetEvent() method creates an Event instance named newEvent with SingleStringSample as the target class.
The Event.CreateQuery() method creates an instance of EventQuery<> named myQuery, which will execute the SQL query and hold the resultset. The sql variable contains an SQL statement that selects all events in the target class with IDs between two parameter values.
The EventQuery<>.AddParameter() method is called twice to assign values to the two parameters.
When the EventQuery<>.Execute() method is called, the specified query is executed for the extent of the target class, and the resultset is stored in myQuery.
By default, all data is fetched for each object in the resultset, and each object is fully initialized. See Defining the Fetch Level for options that limit the amount and type of data fetched with each object.
Processing Query Data
After a query has been executed, the following EventQuery<> methods can be used to access items in the query resultset, and update or delete the corresponding persistent events in the database:
See Accessing Stored Events for a description of how to access and modify persistent events identified by Id or IdKey.
Using Queries: Updating and Deleting Query Data
  myQuery.Execute();   // get resultset
  SingleStringSample currentEvent = myQuery.GetNext();
  while (currentEvent != null) {
    if (currentEvent.name.StartsWith("finished")) {
      myQuery.DeleteCurrent();   // Delete if already processed
    } else {
      currentEvent.name = "in process: " + currentEvent.name;
      myQuery.UpdateCurrent(currentEvent);    // Update if unprocessed
    }
    currentEvent = myQuery.GetNext();
  }
  myQuery.Close();
In this example, the call to EventQuery<>.Execute() is assumed to execute the query described in the previous example (see Creating and Executing a Query), and the resultset is stored in myQuery. Each item in the resultset is a SingleStringSample object.
The first call to GetNext() gets the first item from the resultset and assigns it to currentEvent.
In the while loop, the following process is applied to each item in the resultset:
After the loop terminates, Close() is called to release the native code resources associated with myQuery.
Important:
Always call Close() to avoid memory leaks
It is important to always call Close() on instances of EventQuery<> before they go out of scope or are reused. Failing to close them can cause serious memory leaks because .NET garbage collection cannot release resources allocated by the underlying native code.
Defining the Fetch Level
The fetch level is an Event property that can be used to control the amount of data returned when running a query. This is particularly useful when the underlying event is complex and only a small subset of event data is required.
The following EventQuery<> methods set and return the current fetch level:
The following fetch level values are supported:
Calling Caché Methods from XEP
The following EventPersister methods call Caché class methods:
The following EventPersister methods call Caché functions and procedures (see User-defined Code in Using Caché ObjectScript):
Schema Mapping and Customization
This section provides details about how a .NET class is mapped to a Caché event schema, and how a schema can be customized for optimal performance. The following subjects are discussed:
Schema Import Models
XEP provides two different schema import models: flat schema and full schema. The main difference between these models is the way in which .NET objects are projected to Caché events.
Full object projection preserves the inheritance structure of the original .NET classes, but may have an impact on performance. Flat object projection is the optimal choice if performance is essential and the event schema is fairly simple. Full object projection can be used for a richer set of features and more complex schemas if the performance penalty is acceptable.
The Embedded Object Projection Model (Flat Schema)
By default, XEP imports a schema that projects referenced objects by flattening. In other words, all objects referenced by the imported class are serialized and embedded, and all fields declared in all ancestor classes are collected and projected as if they were declared in the imported class itself. The corresponding Caché event extends %Library.Persistent, and contains embedded serialized objects where the original .NET object contained references to external objects.
This means that a flat schema does not preserve inheritance in the strict sense on the Caché side. For example, consider these three .NET classes:
class A {
    String a;
}
class B : class A {
    String b;
}
class C : class B {
    String c;
}
Importing class C results in the following Caché class:
Class C : %Persistent ... {
     Property a As %String;
     Property b As %String;
     Property c As %String;
}
No corresponding Caché events will be generated for the A or B classes unless they are specifically imported. Event C on the Caché side does not extend either A or B. If imported, A and B would have the following structures:
Class A : %Persistent ... {
     Property a As %String;
}
Class B : %Persistent ... {
     Property a As %String;
     Property b As %String;
}
All operations will be performed only on the corresponding Caché event. For example, calling Store() on objects of type C will only store the corresponding C Caché events.
If a .NET child class hides a field of the same name that is also declared in its superclass, the XEP engine always uses the value of the child field.
The Full Object Projection Model (Full Schema)
The full object model imports a schema that preserves the .NET inheritance model by creating a matching inheritance structure in Caché. Rather than serializing all object fields and storing all data in a single Caché object, the schema establishes a one-to-one relationship between the .NET source classes and Caché projected classes. The full object projection model stores each referenced class separately, and projects fields of a specified class as references to objects of the corresponding Caché class.
Referenced classes must include an attribute that creates a user-defined IdKey (either [Id] or [Index] — see Using IdKeys). When an object is stored, all referenced objects are stored first, and the resulting IdKeys are stored in the parent object. As with the rest of XEP, there are no uniqueness checks, and no attempts to change or overwrite existing data. The data is simply appended at the highest possible speed. If an IdKey value references an event that already exists, it will simply be skipped, without any attempt to overwrite the existing event.
The [Embedded] class level attribute can be used to optimize a full schema by embedding instances of the marked class as serialized objects rather than storing them separately.
Note:
See the FlightLog sample program (listed in XEP Samples) for a demonstration of how to use the full object model.
Schema Customization
In many cases, a schema can be imported for a simple class without providing any meta-information. In other cases, it may be necessary or desirable to customize the way in which the schema is imported. The following sections describe various options for generating customized schemas by adding indexes and overriding the default rules for importing fields:
Using Attributes
The XEP engine infers XEP event metadata by examining a .NET class. Additional information can be specified in the .NET class via attributes, which can be found in the Intersystems.XEP.attributes namespace. As long a .NET object conforms to the definition of an XEP persistent event (see Requirements for Imported Classes), it is projected as a Caché event, and there is no need to customize it.
Some attributes are applied to individual fields in the class to be projected, while others are applied to the entire class:
[Id] (field level attribute)
The value of a field marked with [Id] will be used as an IdKey that replaces the standard object ID (see Using IdKeys). Only one field per class can use this attribute, and the field must be a String, int, or long (double is permitted but not recommended). To create a compound IdKey, use the [Index] attribute instead. A class marked with [Id] cannot also declare a compound primary key with [Index]. An exception will be thrown if both attributes are used on the same class.
The following parameter must be specified:
In the following example, the user-assigned value of the ssn field will be used as the object ID:
using Id = InterSystems.XEP.Attributes.Id;
public class Person {
  [Id(generated=false)]
  Public String  ssn;
  public String  name;
  Public String dob;
}
[Serialized] (field level attribute)
The [Serialized] attribute indicates that the field should be stored and retrieved in its serialized form.
This attribute optimizes storage of serializable fields (including arrays, which are implicitly serializable). The XEP engine will call the relevant read or write method for the serial object, rather than using the default mechanism for storing or retrieving data. An exception will be thrown if the marked field is not serializable.
Example:
using Serialized = InterSystems.XEP.Attributes.Serialized;
public class MyClass {
  [Serialized]
  public  xep.samples.Serialized   serialized;
  [Serialized]
  public  int[,,,]   quadIntArray;
  [Serialized]
  public  String[,]   doubleStringArray;
}

// xep.samples.Serialized:
[Serializable]
public class Serialized {
  public  String  name;
  public  int     value;
}
Is class Serialized also supposed to get an attribute???? Compare this to BXJV.
[Transient] (field level attribute)
The [Transient] attribute indicates that the field should be excluded from import. The marked field will not be projected to Caché, and will be ignored when events are stored or loaded.
Example:
using Transient = InterSystems.XEP.Attributes.Transient;
public class MyClass {
  // this field will NOT be projected:
  [Transient]
  public  String  transientField;

  // this field WILL be projected:
  public  String  projectedField;
}
[Embedded] (class level attribute)
The [Embedded] attribute can be used when a full schema is to be generated (see Schema Import Models). It indicates that a field of this class should be serialized and embedded (as in a flat schema) rather than referenced when projected to Caché.
Examples:
using Embedded = InterSystems.XEP.Attributes.Embedded;
[Embedded]
public class Address {
  String  street;
  String  city;
  String  zip;
  String  state;
}
[Index] (class level attribute)
The [Index] attribute can be used to declare one or more composite indexes.
Arguments must be specified for the following parameters:
Example:
using Index = InterSystems.XEP.Attributes.Index;
using IndexType = InterSystems.XEP.Attributes.IndexType;

[Index(name="indexOne",fields=new string[]{"ssn","dob"},type=IndexType.idkey)]
public class Person {
  public String  name;
  public String dob;
  public String  ssn;
}
Using IdKeys
IdKeys are index values that are used in place of the default object ID. Both simple and composite IdKeys are supported by XEP, and a user-generated IdKey is required for a .NET class that is imported with a full schema (see Importing a Schema). IdKeys on a single field can be created with the [Id] attribute. To create a composite IdKey, add an [Index] attribute with IndexType idkey. For example, given the following class:
  class Person {
    String name;
    int id;
    String dob;
  }
the default storage structure uses the standard object ID as a subscript:
^PersonD(1)=$LB("John",12,"1976-11-11")
The following attribute uses the name and id fields to create a composite IdKey named newIdKey that will replace the standard object ID:
  [Index(name="newIdKey", fields=new String[]{"name","id"},type=IndexType.idkey)]
This would result in the following global structure:
^PersonD("John",12)=$LB("1976-11-11")
XEP will also honor IdKeys added by other means, such as SQL commands (see Using the Unique, PrimaryKey, and IDKey Keywords with Indices in Using Caché SQL). The XEP engine will automatically determine whether the underlying class contains an IdKey, and generate the appropriate global structure.
There are a number of limitations on IdKey usage:
See Accessing Stored Events for a discussion of Event methods that allow retrieval, updating and deletion of events based on their IdKeys.
See SQL and Object Use of Multidimensional Storage in Using Caché Globals for information on IdKeys and the standard Caché storage model. See Defining and Building Indices in Using Caché SQL for information on IdKeys in SQL.
Sample programs IdKeyTest and FlightLog provide demonstrations of IdKey usage (see XEP Samples for details about the sample programs).
Implementing an InterfaceResolver
When a flat schema is imported, information on the inheritance hierarchy is not preserved (see Schema Import Models). This creates a problem when processing fields whose types are declared as interfaces, since the XEP engine must know the actual class of the field. By default, such fields are not imported into a flat schema. This behavior can be changed by creating implementations of Intersystems.XEP.InterfaceResolver to resolve specific interface types during processing.
Note:
InterfaceResolver is only relevant for the flat schema import model, which does not preserve the .NET class inheritance structure. The full schema import model establishes a one-to-one relationship between .NET and Caché classes, thus preserving the information needed to resolve an interface.
An implementation of InterfaceResolver is passed to EventPersister before calling the flat schema import method, ImportSchema() (see Importing a Schema). This provides the XEP engine with a way to resolve interface types during processing. The following EventPersister method specifies the implementation that will be used:
The following example imports two different classes, calling a different, customized implementation of InterfaceResolver for each class:
Schema Customization: Applying an InterfaceResolver
  try {
    myPersister.SetInterfaceResolver(new test.MyFirstInterfaceResolver());
    myPersister.ImportSchema("test.MyMainClass");

    myPersister.SetInterfaceResolver(new test.MyOtherInterfaceResolver());
    myPersister.ImportSchema("test.MyOtherClass");
  }
  catch (XEPException e) {Console.WriteLine("Import failed:\n" + e);}
The first call to SetInterfaceResolver() sets a new instance of MyFirstInterfaceResolver (described in the next example) as the implementation to be used during calls to the import methods. This implementation will be used in all calls to ImportSchema() until SetInterfaceResolver() is called again to specify a different implementation.
The first call to ImportSchema() imports class test.MyMainClass, which contains a field declared as interface test.MyFirstInterface. The instance of MyFirstInterfaceResolver will be used by the import method to resolve the actual class of this field.
The second call to SetInterfaceResolver() sets an instance of a different InterfaceResolver class as the new implementation to be used when ImportSchema() is called again.
All implementations of InterfaceResolver must define the following method:
The following example defines an interface, an implementation of that interface, and an implementation of InterfaceResolver that resolves instances of the interface.
Schema Customization: Implementing an InterfaceResolver
In this example, the interface to be resolved is test.MyFirstInterface:
namespace test {
  public interface MyFirstInterface{ }
}
The test.MyFirstImpl class is the implementation of test.MyFirstInterface that should be returned by the InterfaceResolver:
namespace test {
  public class MyFirstImpl : MyFirstInterface {
    public MyFirstImpl() {}
    public MyFirstImpl(String s) { fieldOne = s; }
    public String  fieldOne;
  }
}
The following implementation of InterfaceResolver returns class test.MyFirstImpl if the interface is test.MyFirstInterface, or null otherwise:
using Intersystems.XEP;
namespace test {
  public class MyFirstInterfaceResolver : InterfaceResolver {
    public MyFirstInterfaceResolver() {}
    public Type GetImplementationType(Type declaringClass, 
           String fieldName, Type interfaceClass) {
      if (interfaceClass == typeof(test.MyFirstInterface)) {
        return typeof(test.MyFirstImpl);
      }
      return null;
    }
  }
When an instance of MyFirstInterfaceResolver is specified by SetInterfaceResolver(), subsequent calls to ImportSchema() will automatically use that instance to resolve any field declared as test.MyFirstInterface. For such each field, the GetImplementationClass() method will be called with parameter declaringClass set to the class that contains the field, fieldName set to the name of the field, and interfaceClass set to test.MyFirstInterface. The method will resolve the interface and return either test.MyFirstImpl or null.
Schema Mapping Rules
This section provides details about how an XEP schema is structured. The following topics are discussed:
Requirements for Imported Classes
The XEP schema import methods cannot produce a valid schema for a .NET class unless it satisfies the following requirements:
The Event.IsEvent() method can be used to analyze a .NET class or object and determine if it can produce a valid event in the XEP sense. In addition to the conditions described above, this method throws an XEPException if any of the following conditions are detected:
Fields of a persistent event can be simple numeric types or their associated System types, objects (projected as embedded/serial objects), enumerations, and types derived from collection classes. These types can also be contained in arrays, nested collections, and collections of arrays.
By default, projected fields may not retain all features of the .NET class. Certain fields are changed in the following ways:
Naming Conventions
Corresponding Caché class and property names are identical to those in .NET, with the exception of two special characters allowed in .NET but not Caché:
Class names are limited to 255 characters, which should be sufficient for most applications. However, the corresponding global names have a limit of 31 characters. Since this is typically not sufficient for a one-to-one mapping, the XEP engine transparently generates unique global names for class names longer than 31 characters. Although the generated global names are not identical to the originals, they should still be easy to recognize. For example, the xep.samples.SingleStringSample class will receive global name xep.samples.SingleStrinA5BFD.