Modelling domains

Persistent types

DataObjects.Net operates with so-called Persistent objects: instances of Entity, Structure and EntitySet types. Persistent objects have transactional state and represent an entity in database (e.g. row in one or several tables, a fragment of row, primary key or relationship between rows).

You should inherit types of your domain model from Structure & Entity types, whereas EntitySet is ready to use out of the box. However, if you want to override its behavior, it is also possible.

Entity

Entity is an object in the domain model that is uniquely identified by its Key` (primary key value). ``Entity object is usually mapped to one row in one or several tables. An Entity instance has its own lifecycle; it may exist independently from any other Entity instance.

Sample entity:

  public class Person : Entity
  {
    [Key]
    [Field]
    public int Id { get; private set; }

    [Field]
    public string FirstName { get; set; }

    [Field]
    public string LastName { get; set; }
    ...

  // 'Person' table contains 3 columns: Id, FirstName & LastName

Reference to Entity is used to express One-to-One association between entities. It is persisted in the database as a foreign key value.

Sample reference to entity:

  public class Animal : Entity
  {
    [Key, Field]
    public int Id { get; private set; }

    [Field]
    public string Name { get; set; }
  }

  public class Person : Entity
  {
    [Key, Field]
    public int Id { get; private set; }

    [Field]
    public Animal Pet { get; set; }
  }

  // 'Person' table contains Pet.Id column as a foreign key to 'Animal' table.

Structure

Structure is also known as “ValueObject”. It represents a part of Entity and doesn’t have its own identity. It is mapped to fragment of the same row as its owning Entity.

Sample structure and entity:

 public class Point : Structure
 {
   [Field]
   public int X { get; set; }

   [Field]
   public int Y { get; set; }
 }

 public class Range : Entity
 {
   [Key, Field]
   public int Id { get; set; }

   [Field]
   public Point Left { get; set; }

   [Field]
   public Point Right { get; set; }
 }

 // 'Range' table contains 5 columns: Id, Left.X, Left.Y, Right.X, Right.Y

EntitySet

EntitySet represents relationship between database tables in object-oriented form (a collection of references to entities). With the help of EntitySet one can express One-to-Many and Many-to-Many kinds of relationship.

EntitySet sample:

  public class Animal : Entity
  {
    [Key, Field]
    public int Id { get; private set; }

    [Field]
    public string Name { get; set; }
  }

  public class Person : Entity
  {
    [Key, Field]
    public int Id { get; private set; }

    [Field]
    public EntitySet<Animal> Pets { get; private set; }
  }

Additional details

Base persistent types are interconnected in the following way:

_images/PersistentTypes.png
  • They directly or indirectly inherit SessionBound type and therefore always bound to a particular Session instance.

  • They have transactional state. State of Entity is a set of values of all its persistent fields, state of EntitySet is a collection of keys of entities. State of a persistent object is valid only within active transaction. When transaction is completed (either committed or rolled back) state of all persistent objects are considered as outdated.

  • Structure & EntitySet types act mostly as wrappers and are instantiated automatically on demand. They implement IFieldValueAdapter contract and always has reference to its Owner (Entity) and to corresponding Field they wrap.

    public interface IFieldValueAdapter
    {
      Persistent Owner { get; }
    
      FieldInfo Field { get; }
    }
    

Entities

Each Entity instance represents exactly one row in database table. Lifecycle of an entity starts when it is instantiated somehow and finishes either after Entity.Remove() method call or after being gathered by .NET garbage collector. During its lifecycle entity can be fetched from database, modified and then persisted back multiple times.

After its first appearance and during the whole session lifecycle entity represents the same row in database table as it was bound to at the moment of appearance. Session instance keeps record of all entities that have been read from database within its scope. If during fetch request execution entity is found by the given primary key in session’s data context – it is immediately returned; otherwise fetch query to database is executed. This means that you always get the same entity instance for the given primary key within the same session.

Every Entity instance knows its primary key – the primary key of database row it represents. Primary key value can be accessed through Entity.Key property. It is immutable and can’t be changed in any way. As it is immutable, it is safe to use key to cache entities in various types of caches, etc.

Entity.PersistenceState property indicates the current persistence state of entity. Entity can be in Synchronized, New, Modified and Removed state. Synchronized option goes for state when it is the same as state of corresponding row in the database. Persistence state indicates what should be done to synchronize entity with corresponding row in database.Here is the state transition diagram that represents the available paths for entity persistence state:

_images/PersistenceState.png

Entity lifecycle

There are few basic operation with entities (Entity type):

  • Construction. DataObjects.Net allows you to use regular constructors there, however, it is recommended to pass reference to Session to constructor.

    using (var session = domain.OpenSession()) {
      using (var tx = session.OpenTransaction()) {
        var person = new Person(session);
        tx.Complete();
      }
    }
    

    As an alternative, DataObjects.Net can acquire Session from currently active Session, so session activation is required. This is a bit outdated behavior, but it is still supported.

    using (var session = domain.OpenSession()) {
      using (session.Activate()) {
        using (var tx = session.OpenTransaction()) {
          var person = new Person(); // Note the empty constructor
          tx.Complete();
        }
      }
    }
    
  • Materialization is process of construction of .NET object (Entity) representing stored entity in your code. It may happen on:

    • Single item fetching. It happens when you resolve an Entity by its Key. Each DataObjects.Net Session maintains 1st level cache (L1 cache further) operating as identity map ensuring that if you maintain a reference to an entity e, any way of retrieving it by its key (e.Key) will return the same object.

    • Query result enumerating. DataObjects.Net materializes results of queries on demand, but this process is batched: when you move a result enumerator to the next item, DataObjects.Net returns either already cached item, or materializes next bulk of them. Bulk size increases twice each time you’re reaching when next bulk must be produced, starting from 16 and finishing at 1024 items (it stops growing up at that number). This is done to materialize query result concurrently, but don’t waste too much time on excessive job. DataObjects.Net assumes:

      • Result of any query you execute will, likely, be fully processed (i.e. enumerated).
      • If not, DataObjects.Net ensures it will materialize less then 1024 excessive entities from it, but not more.
      • If MultipleActiveResultSets option is turned on, underlying DbDataReader will pull out less than 1024 additional rows.

      This, in conjunction with default L1 cache policy, means that DataObjects.Net allows to process very long query results.

  • Modification happens when you change properties of entities or properties of structures they contain. Changes are not persisted immediately – DataObjects.Net decide when to perform a flush by its own. Currently such an automatic flush happens:

    • Before transaction commit
    • Before any query (except future queries). Note that reference search queries may run before entity removal.
    • When DataObjects.Net detects there are lots of changes. Currently limit is set to 250 changed entities and can be changed via SessionConfiguration.EntityChangeRegistrySize property. This is done to ensure SQL Server will get its job done earlier, and thus will complete them earlier to shorten the transactions. This is efficient, since shortly flushes will be asynchronous.
  • Removal. Removing entity is simple. Just call Entity.Remove() method and entity will be marked as removed. At the next persist operation its corresponding row will be deleted from database table. DataObjects.Net makes appropriate referential integrity checks before entity removal and verifies that all referential constraints are met. Referential constraints might include cascade removal of dependent entities, destruction of references to entities being removed or denial of removal. These referential integrity rules are declared during domain modeling in association declaration and automatically executed by DataObjects.Net. For detailed information see “Modeling domains” chapter.

    Note

    By default, after entity is marked as removed its persistent fields except identity fields become inaccessible. To change this behavior set SessionOptions.ReadRemovedObjects to true.

Hierarchies of entities

Hierarchy represents a set of entity classes that inherit from hierarchy root where identity fields are usually defined. All members of hierarchy share identity fields, inheritance mapping scheme, identity provider and type discriminator, if any. Hierarchy can contain one or more members, hierarchy depth is unrestricted. Number of hierarchies within Domain model is also unrestricted. The only restriction is that member of one hierarchy can’t belong to another hierarchy.

Hierarchy is entirely defined by its root element. This is how we do it:

[HierarchyRoot]
public class Document : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public DateTime Date { get; set; }

  [Field]
  public int Number { get; set; }

  public Document(Session session)
   : base(session) {}
}

Note that the class is marked with HierarchyRootAttribute and it contains field that is marked with KeyAttribute. Thus we explicitly define structure of key for this hierarchy (and by that for every class that belongs to this hierarchy). Also remember, that all root’s descendants belong to the same hierarchy. Class could belong to one hierarchy only at a time. This means that an attempt to define a new hierarchy root inside existing hierarchy is considered as error.

Inheritance mapping schemes

The most important option that is exposed by HierarchyRootAttribute is inheritance mapping scheme. DataObjects.Net 4 supports the following hierarchy mapping schemes:

  • ClassTable is the default one. It represents an inheritance hierarchy of classes with one table for each class. It is ideal for deep inheritance hierarchies and queries returning base classes. This case implies joins + a single base table for the whole hierarchy.

  • SingleTable represents an inheritance hierarchy of classes as a single table that has columns for all the fields of the various classes. This kind of mapping is more preferable for tiny inheritance hierarchies, or for hierarchies where there is a set of abstract classes and a non-abstract single leaf.

  • ConcreteTable represents an inheritance hierarchy of classes with one table per concrete class in the hierarchy. This kind is ideal when you always query for exact types of objects stored there. I.e. you query not for Animal, but for Dog (which is a leaf type in hierarchy). When you query for Animal here, there will be UNION in query.

    Note

    If you feel unsure about terms like “hierarchy mapping schema”, we recommend you to visit: Martin Fowler’s online catalogue of Patterns of Enterprise Application Architecture There are short, complete descriptions of many ORM related concepts.

This is how we can define inheritance mapping for the given hierarchy:

[HierarchyRoot(InheritanceSchema = InheritanceSchema.ClassTable)]
public class Document : Entity
{
  ...
}

Managing primary indexes

Non-clustered primary indexes

By default, DataObjects.Net creates tables for a given hierarchy and makes their primary keys as clustered (if underlying storage supports clustered indexes). Setting HierarchyRoot(Clustered = false) will prevent DataObjects.Net from creating clustered primary index for all tables in the hierarchy. If no other index is configured as clustered, then every table in the hierarchy will be created as Heap table in terms of MS SQL Server.

[HierarchyRoot(Clustered = false)]
[Index("Login", Unique = true, Clustered = true)]
public class Person : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  [Field]
  public string Login { get; set; }
}

Composite primary keys

Sometimes keys are more complex than just one identity field. In such scenarios we need to set up the order of columns in primary key index. This can be done with the help on KeyAttribute.Position property. Note that position index starts with 0.

[KeyGenerator(KeyGeneratorKind.None)]
[HierarchyRoot]
public class OrderDetails : Entity
{
  [Field, Key(Position = 0)]
  public Order Order { get; private set; }

  [Field, Key(Position = 1)]
  public Product Product { get; private set; }

  [Field]
  public decimal UnitPrice { get; set; }

  [Field]
  public short Quantity { get; set; }
}

In this example we define composite primary key with two columns called Order and Product. By default, values in primary index are stored in ascending order. To manioulate the way values are stored, use KeyAttribute.Direction property.

Including TypeId into key

DataObjects.Net provides a feature to automatically include Type identifier into key for a given hierarchy. This can be achieved in the following way:

[HierarchyRoot(IncludeTypeId = true)]
public class Person : Entity
{
  [Field, Key]
  public int Id { get; private set; }

The option tells DataObjects.Net to silently add one additional field named TypeId, type of int to identity columns of the hierarchy, so primary key will also contain TypeId column. TypeId is an identifier of persistent type within DataObjects.Net domain. Usually, type identifiers can be found in Metadata.Type table. So the idea is to include that type identifier into primary key so every foreign key that references a class from the given hierarchy will include both identity fields and type identifier. This theoretically might increase the speed of fetching referenced entities as DataObjects.Net will know the exact type of entity referenced and will produce a query that will fetch the entire entity for one roundtrip.

Mapping to specific table

With the help of TableMappingAttribute entity can be mapped to a table with specific name, e.g.:

[TableMapping("Persons")]
public class Author : Entity
{
  ...
}

This code snippet tells DataObjects.Net to map persistent class Author to a table Persons. That’s the only purpose of this attribute.

Events

Entity type provides us with the list of protected virtual methods that can be used in descendant types for any appropriate purposes like tracing, auditing, validating, adjusting values, etc. Usually OnGetting, OnSetting methods are used for validation & security related checks, whereas OnGet and OnSet contain mostly logging/auditing logic. Here they are with short description.

  • OnInitialize()
    Called when instance is initialized (constructed) or materialized from database.
  • OnInitializationError(Exception error)
    Called on instance initialization error.
  • OnGettingFieldValue(FieldInfo field)
    Called before field value is about to be read.
  • OnGetFieldValue(FieldInfo field, object value)
    Called when field value has been read.
  • OnSettingFieldValueAttempt(FieldInfo field, object value)
    Called before field value is about to be set. This event is raised on any set attempt (even if new value is the same as the current one).
  • OnSettingFieldValue(FieldInfo field, object value)
    Called before field value is about to be changed. This event is raised only on actual change attempt (i.e. when new value differs from the current one).
  • OnSetFieldValue(FieldInfo field, object oldValue, object newValue)
    Called when field value has been changed.
  • AdjustFieldValue(FieldInfo field, object value)
    Called when value is read from the field and before it is returned to caller.
  • AdjustFieldValue(FieldInfo field, object oldValue, object newValue)
    Called before value is written to the field.
  • OnRemoving()
    Called when entity is about to be removed.
  • OnRemove()
    Called after entity marked as removed.
  • OnValidate()
    Called when entity should be validated. Override this method to perform custom object validation.

Structures

Structure represents a part of entity, so you may think of it as of a complex field that has a set of nested fields.

public class Point : Structure
{
  [Field]
  public int X { get; set; }

  [Field]
  public int Y { get; set; }
}

[HierarchyRoot]
public class Range : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public Point Left { get; set; }

  [Field]
  public Point Right { get; set; }
}

Here is a Range entity type that contains 2 fields of Point type. Point type is a Structure descendant and values of its instances are stored inside Range entity.

This “Range-Point” model is mapped to database schema in the following way:

_images/Structures-DatabaseMapping.png

Here is how we work with fields of Point type:

var range = new Range();

// Fields of Structure type are always not null
Assert.IsNotNull(range.Left);

// Being unassigned nested fields contain default values like any other fields
Assert.AreEqual(0, range.Left.X);
Assert.AreEqual(0, range.Left.Y);

// Field by field assignment
range.Left.X = 5;
range.Left.Y = 10;

// Another way of assignment
var point = new Point(5, 10);
range.Left = point;

Structures can participate in LINQ queries as any other persistent types. You can use it inside IQueryable.Where clause or you can select structure instances, like this:

var points = session.Query.All<Range>().Select(r => r.Left);
// or
var ranges = session.Query.All<Range>().Where(r => r.Left == new Point(0, 10));

Implementation details

Structure is implemented according to Proxy pattern, automatically redirecting all calls to its owner. It doesn’t contain any real values, instead, structure is bound to the corresponding segment of its owner’s state as it is shown on the diagram:

_images/Structures-Implementation.png

Every modification made to Point instance is transparently applied to the state of its owner (Range): e.g. when you set Point.X to 10 you actually assign 10 to Range.Left.X property.

Range range = new Range();
range.Left.X = 0;
range.Left.Y = 0;

Point point = range.Left;
Assert.IsTrue(ReferenceEquals(point, range.Left));

// Let's modify "point" instance.
// "range.Left" will be changed automatically
point.X = 10;
Assert.AreEqual(10, range.Left.X);

According to its Proxy nature, property of Structure type always returns the same instance of Structure. In case when property itself is not assigned, it returns structure with default values. This also means that property of Structure type never returns null.

Assignment behavior

Structure has copy-on-set behavior, e.g. when you assign some Point instance to Range.Right property, all Point fields are copied into appropriate Range.Right fields. This happens when assignment is done to persistent properties of Structure type only. In all other cases you just assign reference to Structure instance (well-known .NET reference behavior).

range.Right = range.Left;

// Instances are not equal although they have the same values
Assert.IsFalse(ReferenceEquals(range.Right, range.Left));
Assert.AreEqual(range.Right.X, range.Left.X);
Assert.AreEqual(range.Right.Y, range.Left.Y);

If you want to reset property of Structure type then you should assign to it Structure instance with default values. You mustn’t assign null to property of Structure type, it throws ArgumentNullException.

// Wrong way. Throws ArgumentNullException
range.Left = null;

// Right way
range.Left = new Point();

Unbound structures

Usually, you deal with Structure instances that are bound to their owner (Entity). But structures can exist autonomously — as a regular .NET object (though in this case it can’t be persisted). It can be used in assignment or as a part of Where clause in LINQ queries.

var point = new Point(1, 5);
var range = new Range();
range.Left = point;

var ranges = Query<Range>.All.Where(r => r.Left == point);

Known limitations

1. Aggregation limitation. As structure is entirely embedded into its owner’s table, it has aggregation limitation. This means that you can’t use the following approach while dealing with structures:

public class Vertex : Structure
{
  [Field]
  public Vertex Vertex { get; set; }
}

This case can’t be mapped to database schema because it leads to recursion. No matter how deep is the loop, it simply can’t be mapped.

2. Inheritance limitation Structure supports inheritance, but again, as it is entirely embedded into its owner’s table, there is inheritance limitation. Say you have the following model:

public class Pair : Structure {

  [Field]
  public int First { get; set; }

  [Field]
  public int Second { get; set; }
}

public class Triplet : Pair {

  [Field]
  public int Third { get; set; }

}

[HierarchyRoot]
public class Container : Entity {

  [Field, Key]
  public int Id { get; private set; }

  [Field]
  public Pair Value { get; set; }
}

This model is mapped to database schema as following:

_images/Structures-InheritanceMapping.png

Although you can legally assign Triplet instance to Container.Value property (you won’t get any compilation error), there is not enough space to persist entire Triplet instance as corresponding Container table has only Value.First & Value.Second fields. In such case InvalidOperationException will be thrown. You must explicitly construct new Pair instance to make an assignment, like this:

container.Value = new Pair(triple.First, triple.Second);
  1. Other limitations
  • Structure can’t contain persistent field of EntitySet<T> type.
  • Structure can’t act as identity field.

Events

Structure types exposes the same events as Entity type except OnRemoving and OnRemove events as they have no sense in context of structures. See Entity events for full list.

Fields

Field in terms of persistent model is an automatic property marked with FieldAttribute. FieldAttribute‘s presence is required; otherwise property is skipped during DataObjects.Net domain building procedure.

[HierarchyRoot]
public class Document : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public DateTime Date { get; set; }

  [Field]
  public int Number { get; set; }

  public Document(Session session)
   : base(session) {}
}

Supported types

The following types are supported:

  • sbyte, sbyte?, byte, byte?, short, short?, ushort, ushort?, int, int?, uint, uint?, long, long?, ulong, ulong?
  • single, single?, double, double?, decimal, decimal?
  • bool, bool?
  • string, char
  • DateTime, DateTime?, TimeSpan, TimeSpan?
  • Guid, Guid?
  • enums, nullable enums
  • byte[]
  • Key
  • Descendants of Structure
  • Descendants of Entity
  • EntitySet<TItem> where TItem : IEntity and its descendants

FieldAttribute

With the help of FieldAttribute developer controls various options of persistent fields:

  • Length – to set length of corresponding table column for string or byte[] fields.

    [Field(Length = 256)]
    public string Name { get; set; }
    

    This code snippet will create underlying column of varchar(256) data type (for MS SQL Server).

    To indicate that stream varcharmax for string type / varbinarymax data type (for MS SQL Server) or their analogues (for other DB servers) should be used, set value of Field.Length property to Int32.MaxValue.

  • Scale – to set scale of corresponding table column for single, double, decimal fields.

  • Precision – to set precision of corresponding table column for single, double, decimal fields.

    [Field(Precision = 15, Scale = 2)]
    public decimal Sum { get; set; }
    
  • LazyLoad – to indicate that field must be loaded in lazy manner. This options is meaningful for fields that hold potentially large values, such as huge string & byte[].

    [Field(Length = Int32.MaxValue, LazyLoad = true)]
    public string Article { get; set; }
    

    Note that we set up Field.Length property so that field will be mapped to data type with maximal capacity (varcharmax for MS SQL Server) so we can store text of the entire article in it.

  • Nullable – to indicate that field is nullable. This options has sense for reference fields only and usually used to set Nullable to false to indicate that reference field can’t hold null value.

    [Field(Length = 256, Nullable = false)]
    public string Name { get; set; }
    

    By default, underlying columns for fields of reference types (string, byte[], Entity, etc) are made nullable, e.g. [Name] [nvarchar](256) NULL. To indicate that the column must not be nullable, set Field.Nullable to false.

  • Indexed – to indicate that index on this field should be created. More about indexes can be found in chapter about indexes.

  • DefaultValue – specifies the default value for the field. This is the same as if you set the value of this field in constructor.

  • DefaultSqlExpression – specifies the default sql expression for the field. This is native sql expression which is used to get default value for column associated with the field, e.g. GETDATE() for MS SQL Server to get current date on server-side. IMPORTANT that``DefaultSqlExpression`` has higher prority than DefaultValue

  • NullableOnUpgrade – to indicate that during upgrade the underlying column of the field must be nullable.

Mapping to specific column

With the help of FieldMappingAttribute field can be mapped to a column with specific name, e.g.:

[Field]
[FieldMapping("PersonName")]
public string FullName { get; set; }

This code snippet tells DataObjects.Net to map persistent field FullName to a column PersonName. That’s the only purpose of this attribute.

Limitations

Indexers are not allowed, e.g. the following code throws DomainBuilderException.

[Field]
public string this[int index]

Identity field can’t have setter other than private. Identity field is a field that is marked with KeyAttribute.

[Key, Field]
public int Id { get; private set; }

Field of EntitySet<T> type can’t have setter other than private. It is a virtual collection that serves association between entities, it is instantiated automatically at the first access.

[Field]
public EntitySet<Pet> Pets { get; private set; }

Structure can’t contain field of EntitySet<T> type. The main reason for this limitation is that as EntitySet<T> expresses the relation between exactly two entities and if there can be a scenario when Structure instance is unbound of any entity, then there is no any way to provide EntitySet<T>‘s responsibility for this case. Anyway, there is a probability that this limitation will disappeared in the future versions of DataObjects.Net.

Identity field can’t be of type Structure or EntitySet<T>.

EntitySets

Overview

EntitySet type is designed to express associations with One-to-Many and Many-to-Many cardinality between two persistent types in object-oriented form.

[HierarchyRoot]
public class Meeting : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public EntitySet<Person> Participants { get; private set; }
}

EntitySets behave like HashSet<T> in .NET – they provide unordered set of items they store. These items are:

  • Either entities containing a back reference to its owner, and owner’s EntitySet field is marked with [Association(PairTo=...)] attribute pointing to such reference.
  • Or entities containing no back reference to its owner. In this case DataObjects.Net registers and uses a special intermediate entity type to maintain many-to-many relationship for this EntitySet. Actually it is an ancestor of internal EntitySetItem<TMaster, TSlave> created in runtime.

Operations

You can perform the following operations with EntitySet:

  • Get count of entities contained in EntitySet. If EntitySet content isn’t loaded at this moment, DataObjects.Net runs a query loading up to 32 items from it. If it gets less than 32 items by this request, it considers EntitySet is fully loaded, so further operations with it will not lead to any queries. Otherwise it caches keys of loaded items and run one more query getting actual count of items. In this case EntitySet is partially loaded. Contains call on it may or may not lead to query; Count call on it will not lead to query; its enumeration will lead to query.
  • Check existence (using Contains method) of a particular entity there. If EntitySet is not loaded, nearly the same process as for Count property access will happen, but in this case DataObjects.Net won’t try to load count of items there. Instead, it will perform a query checking presence of specified item, and will cache its result.
  • Enumerate EntitySet
  • Add an entity to EntitySet
  • Remove an Entity from it
  • Clear the EntitySet
var meeting = new Meeting();
var john = new Person();
var michael = new Person();

meeting.Participants.Add(john);
meeting.Participants.Add(michael);

Assert.IsTrue(meeting.Participants.Contains(john));
Assert.IsTrue(meeting.Participants.Contains(michael));
Assert.AreEqual(2, meeting.Participants.Count);

meeting.Participants.Clear();
Assert.AreEqual(0, meeting.Participants.Count);

No additional work is required. EntitySet’s state is persisted and loaded fully transparently.

Querying EntitySet

EntitySet implements IQueryable<T> interface, hence all LINQ operations are applicable to it as well.

var vips = meeting.Participants.Where(p => p.IsVIP);

Events

EntitySet type exposes the list of protected virtual methods that can be used in descendant types for any appropriate purposes like tracing, auditing, validating, etc.

  • OnInitialize()
    Called when EntitySet is initialized. Usually this happens on first access to the corresponding field.
  • OnAdding(Entity item)
    Called when item is about to be added to EntitySet.
  • OnAdd(Entity item)
    Called when item is added to EntitySet.
  • OnRemoving(Entity item)
    Called when item is about to be removed from EntitySet.
  • OnRemove(Entity item)
    Called when item is removed from EntitySet.
  • OnClearing()
    Called when EntitySet is about to be cleared.
  • OnClear()
    Called when EntitySet is cleared.
  • OnValidate()
    Called when EntitySet is about to validated.

Associations

In real life, there are lots of associations between objects, many of them are also bidirectional. DataObjects.Net provides developers with means to effectively express these associations, moreover, as DataObjects.Net ships rich ORM & BLL layers in one box, it makes possible to supports the concepts of “paired” (or autosynchronized) associations and referential integrity.

In fact, there are 3 types of associations, such as: One-to-One, One-to-Many, Many-to-Many.

One-to-One association is expressed as a reference to another entity. Say, Book might have a reference to its Author:

[HierarchyRoot]
public class Book : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public Author Author { get; set; }
}

But Author might have written more than exactly one book. In this case One-to-Many association in form of EntitySet<Book> appears:

[HierarchyRoot]
public class Author : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public EntitySet<Book> Books { get; private set; }
}

In case we decided that one book can be written by several authors then Many-to-Many association is the right choice. This scenario imply the usage of EntitySet<T> on both end of the association:

[HierarchyRoot]
public class Book : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public EntitySet<Author> Authors { get; private set; }
}

[HierarchyRoot]
public class Author : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public EntitySet<Book> Books { get; private set; }
}

Paired associations

In most ORM frameworks, especially in those that operate with POCO, developer have to manually synchronize bidirectional association, mixing pure domain model artifacts (associations) with concrete implementation, e.g.:

var book = new Book();
var author = new Author();

book.Authors.Add(author);
author.Books.Add(book);

Dealing with bidirectional associations in DataObjects.Net is like a piece of cake, all developer should do is just indicate that one association is paired to another and that’s it!

[HierarchyRoot]
public class Book : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public EntitySet<Author> Authors { get; private set; }
}

[HierarchyRoot]
public class Author : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  [Association(PairTo = "Authors")]
  public EntitySet<Book> Books { get; private set; }
}

In this example Author.Books and Book.Authors collections become automatically synchronized (“paired”). Manual synchronization is not required anymore.

var book = new Book();
var author = new Author();

// This line automatically executes author.Books.Add(book)
book.Authors.Add(author);

Certainly, the feature is available for all types of bidirectional associations: One-to-One, One-to-Many, Many-to-Many.

Referential integrity and business rules

Another invaluable feature in DataObjects.Net is integrated referential integrity with comprehensive business rules support. Say you have the following model:

_images/Order-OrderItem.png

where unidirectional association between Order and OrderItem types is declared (Order instance has reference to one or more OrderItem instances, but OrderItem instances don’t have any reference to Order instance). Generally, the association is expressed using One-to-Many association pattern:

[HierarchyRoot]
public class Order : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  public EntitySet<OrderItem> Items { get; private set; }
}

[HierarchyRoot]
public class OrderItem : Entity
{
  [Key, Field]
  public int Id { get; private set; }
}

But what if the association requires some business rules to be applied? Say, OrderItem instance can’t be removed in case it is referenced by Order instance (contained in its Order.Items collection).

Often developer has to manually implement association-related business rules: for example, execute a query to check whether there is at least one Order instance that references the specified OrderItem instance. Seems not difficult. What about cascade removal of entire graph of entities? Not so simple. But the real problem is not in the difficulty itself, but in business rules hard coding. It is another attempt to mix pure domain modeling with concrete implementation, and this is conceptually bad.

Luckily, DataObjects.Net makes the definition of association-related business rules really easy task. It introduces the notion of RemovalAction. Each end of an association can be marked with appropriate removal action. To make a distinction between association ends, one of them is named as “owner end”, and another as “target end”. Owner end is located in class where the association is declared. In Order-Detail model Order is the owner end of association and OrderItem is target end, correspondingly.

_images/Order-OrderItem2.png

Using these features developer can easily declare the business rules for an association:

[HierarchyRoot]
public class Order : Entity
{
  [Key, Field]
  public int Id { get; private set; }

  [Field]
  [Association(
    OnOwnerRemove = OnRemoveAction.Cascade,
    OnTargetRemove = OnRemoveAction.Deny)]
  public EntitySet<OrderItem> Items { get; private set; }
}

[HierarchyRoot]
public class OrderItem : Entity
{
  [Key, Field]
  public int Id { get; private set; }
}

By applying AssociationAttribute with specified options on Order.Items field we express in declarative way that the association must follow the following business rules:

  • On attempt to remove Order instance all contained OrderItem instances must be removed also.
  • On attempt to remove OrderItem instance a check for existence of any Order instance that references OrderItem must be executed. If any Order instance is found then ReferentialIntegrityViolationException must be thrown.

Besides Cascade & Deny OnRemoveAction enum has the following options:

  • Clear – is used to indicate that reference must be cleared. No additional actions should be executed.
  • None – is used when reference must preserve its value. This option is intended to be used with great care.

Indexes

Introduction

In DataObjects.Net indexes play dual role. Firstly, indexes can be used to express business rules of entity uniqueness. Secondly, indexes are used to define physical indexes in persistent storage to speedup execution of queries. To indicate that an index is required developer should apply IndexAttribute on entity type or set FieldAttribute.Indexed property.

[Index("ProductName")]
public class Product : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  [Field(Length = 40)]
  public string ProductName { get; set; }

For simple 1-field indexes the alternate way might be more useful to adopt.

public class Product : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  [Field(Length = 40, Indexed = true)]
  public string ProductName { get; set; }

Number of indexes for particular entity class is unrestricted.

Unique indexes

Index can be marked as unique.

[Index("Login", Unique = true)]
public class Person : Entity
{
[Field, Key]
public int Id { get; private set; }

[Field]
public string Login { get; set; }

Indexes with multiple fields

Index can contain more than one field. These fields are named as key fields.

[Index("FirstName", "LastName")]
public class Person : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  [Field(Length = 20)]
  public string LastName { get; set; }

  [Field(Length = 10)]
  public string FirstName { get; set; }

Managing the sort order

By default values of key fields are sorted in ascending order. In order to indicate that descending order should be used for a particular field, ”:DESC” suffix should be added to the name of key field.

[Index("HireDate:DESC")]
public class Person : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  [Field]
  public DateTime HireDate { get; set; }

Included fields

“Included fields” notion is also supported. These are fields that physically stored in index in addition to key fields.

[Index("Login", IncludedFields = new[]{"FirstName", "LastName"})]
public class Person : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  [Field]
  public string Login { get; set; }

  [Field(Length = 20)]
  public string LastName { get; set; }

  [Field(Length = 10)]
  public string FirstName { get; set; }

Custom mapping

Custom mapping name can be specified for index.

[Index("Login", Name = "IX_LOGIN")]
public class Person : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  [Field]
  public string Login { get; set; }

Clustered and non-clustered indexes

Index can be clustered or not. This is particular useful in scenario when you don’t want to have corresponding table to be physically arranged by primary key, but rather by another field.

[HierarchyRoot(Clustered = false)]
[Index("Login", Unique = true, Clustered = true)]
public class Person : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  [Field]
  public string Login { get; set; }

Filtered/partial indexes

Index can be filtered/partial. Filtered index is an index which has some condition applied to it so that it includes a subset of rows in the table. This allows the index to remain small, even though the table may be rather large, and have extreme selectivity.

[Index("FacebookName", Filter = "FacebookNameFilter")]
public class User : Entity
{
  private static Expression<Func<User, bool>> FacebookNameFilter()
  {
    return user => user.FacebookName != null;
  }

  [Field, Key]
  public int Id { get; private set; }

  [Field(Nullable = true)]
  public string FacebookName { get; set; }

To set the condition for filtered index we declare static Expression that takes instance of Entity and returns true or false. The expression is parsed during Domain build process and the translated result is used to configure the corresponding physical index on storage level. The expression physically can be declared in type other than Entity itself. To do the trick set the IndexAttribute.FilterType property.

[Index("FacebookName",
  Filter = "FacebookNameFilter",
  FilterType = typeof(FilterExpressionLibrary))]
public class User : Entity
{
  [Field, Key]
  public int Id { get; private set; }

  [Field(Nullable = true)]
  public string FacebookName { get; set; }
}

public static class FilterExpressionLibrary
{
  private static Expression<Func<User, bool>> FacebookNameFilter()
  {
    return user => user.FacebookName != null;
  }
  ...
}

Automatically maintained indexes

DataObjects.Net automatically creates and maintains the following types of indexes:

  • Primary indexes. These type of indexes contain key fields. Every entity type has one primary index. This index is unique and clustered if the underlying storage supports index clustering.
  • Indexes for reference fields. Indexes of this kind are not unique.

Services

Introduction

Typically, a service is an operation offered as an interface that stands alone in the model, without encapsulating state, as entities and structures do. Statelessness here means that any client can use any instance of a particular service without regard to the instance’s individual history.

DataObjects.Net offers the following ecosystem for modelling and utilizing services:

  • Declarative approach. Service classes marked with a special attribute, are found and registered in domain model automatically.
  • Singleton and transient service modes are supported, as well as named service implementations.
  • Two built-in service containers are provided: Domain.Services and Session.Services.
  • Built-in service containers can be extended with any custom IoC container.

Built-in service containers

As it was mentioned, DataObjects.Net contains two built-in service containers where from instances of services can be obtained. According to their names, they are bound to different lifetime scopes: Domain.Services container is domain-level one and its lifetime is equal to lifetime of domain, whereas Session.Services is constructed for every single Session and is disposed with it.

Domain.Services acts like a parent container for Session.Services so in case a service can’t be resolved through Session.Services, the search continues in Domain.Services container.

Domain.Services contains services that implements IDomainService interface, and Session.Services is a place for services of ISessionService type.

A service can be resolved through the containers via Get<TService> or Get(Type contractType) methods.

var domainService = domain.Services.Get<IMyDomainService>();
domainService.DoWork();

using (var session = domain.OpenSession()) {
  var sessionService = session.Services.Get<IMySessionService>();
  sessionService.DoWork();
}

In addition, you might want to obtain multiple service implementations from container. This can be done with the help of GetAll<TService> or GetAll(Type contractType) methods.

var domainServices = domain.Services.GetAll<IMyDomainService>();
foreach (var service in domainServices)
  service.DoWork();

using (var session = domain.OpenSession()) {
  var sessionService = session.Services.GetAll<IMySessionService>();
  foreach (var service in sessionServices)
    service.DoWork();
}

Automatic service registration

Minimal requirements for your service to be available in Domain.Services or Session.Services containers is to implement IDomainService or ISessionService correspondingly.

// Declaring domain service contract
public interface IMyDomainService : IDomainService
{
  void DoWork();
}
// Declaring session service contract
public interface IMySessionService : ISessionService
{
  void DoWork();
}

// Implementing domain service contract
[Service(typeof(IMyDomainService))]
public class MyDomainService : IMyDomainService
{
  public void DoWork()
  {
    // do some work here
  }
}

// Implementing session service contract
[Service(typeof(IMySessionService))]
public class MySessionService : IMySessionService
{
  public void DoWork()
  {
    // do some work here
  }
}

Note the presence of ServiceAttribute on service implementation.

Transient and singleton services

By default, every single time you resolving a service through Domain.Services or Session.Services containers, you get a new instance of the service requested. These instances are called transient as every time they are constructed from scratch. To override this behavior, you should indicate that only one particular implementation of a service should exist within a scope of a container. It is call Singleton.

[Service(typeof(IMyDomainService), Singleton = true)]
public class MyDomainService : IMyDomainService
{
  public void DoWork()
  {
    // do some work here
  }
}

Note usage of Singleton = true. This exactly means that there must be no more than only one instance of the service in every single container.

Named services

In case you have serveral implementations of the same contract, you may want to get a particular one from the container by some tag or name. To do that, you should set up the corresponding Service.Name property in attribute.

// Service contract
public interface IMyDomainService : IDomainService
{
  void DoWork();
}

// Contract implementations
[Service(typeof(IMyDomainService), Name = "Super")]
public class MySuperService : IMyDomainService
{
  public void DoWork()
  {
    // do some work here
  }
}

[Service(typeof(IMyDomainService), Name = "Fancy")]
public class MyFancyService : IMyDomainService
{
  public void DoWork()
  {
    // do some work here
  }
}

// Obtaining named services from container
var superService = domain.Services.Get<IMyDomainService>("Super");
var fancyService = domain.Services.Get<IMyDomainService>("Fancy");

Note usage of Name property. This name is associated with a particular service implementation.

Domain-bound and Session-bound services

DataObjects.Net IoC containers can automatically resolve references to Domain and Session instances, making sort of dependency injection in service constructors. To utilize the feature you should first inherit from DomainBound type and mark the constructor that takes instance of Domain as argument with ServiceConstructor attribute.

[Service(typeof(IMyDomainService))]
public class MyDomainService : DomainBound, IMyDomainService
{
  public void DoWork()
  {
    // do some work
  }

  [ServiceConstructor]
  public MyDomainService(Domain domain)
    : base(domain)
  {}
}

Session-bound services are declared in similar way. The only difference is inheritance from SessionBound type.

[Service(typeof(IMySessionService))]
public class MySessionService : SessionBound, IMySessionService
{
  public void DoWork()
  {
    // do some work
  }

  [ServiceConstructor]
  public MySessionService(Session session)
    : base(session)
  {}
}