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:
They directly or indirectly inherit
SessionBoundtype and therefore always bound to a particularSessioninstance.They have transactional state. State of
Entityis a set of values of all its persistent fields, state ofEntitySetis 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&EntitySettypes act mostly as wrappers and are instantiated automatically on demand. They implementIFieldValueAdaptercontract 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:
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
Sessionto 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
Sessionfrom currently activeSession, 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
Entityby itsKey. Each DataObjects.NetSessionmaintains 1st level cache (L1 cache further) operating as identity map ensuring that if you maintain a reference to an entitye, 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
MultipleActiveResultSetsoption is turned on, underlyingDbDataReaderwill 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.EntityChangeRegistrySizeproperty. 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.ReadRemovedObjectstotrue.
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:
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:
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:
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);
Other limitations
Structurecan’t contain persistent field ofEntitySet<T>type.Structurecan’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,charDateTime,DateTime?,TimeSpan,TimeSpan?Guid,Guid?enums,nullable enumsbyte[]KeyDescendants of
StructureDescendants of
EntityEntitySet<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 forstringorbyte[]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
varcharmaxfor string type /varbinarymaxdata type (for MS SQL Server) or their analogues (for other DB servers) should be used, set value ofField.Lengthproperty toInt32.MaxValue.Scale– to set scale of corresponding table column forsingle,double,decimalfields.Precision– to set precision of corresponding table column forsingle,double,decimalfields.[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 hugestring&byte[].[Field(Length = Int32.MaxValue, LazyLoad = true)] public string Article { get; set; }
Note that we set up
Field.Lengthproperty so that field will be mapped to data type with maximal capacity (varcharmaxfor 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 setNullabletofalseto indicate that reference field can’t holdnullvalue.[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, setField.Nullabletofalse.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 thanDefaultValueNullableOnUpgrade– 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
EntitySetfield 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 internalEntitySetItem<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.
Containscall on it may or may not lead to query;Countcall on it will not lead to query; its enumeration will lead to query.Check existence (using
Containsmethod) of a particular entity there. If EntitySet is not loaded, nearly the same process as forCountproperty 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:
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.
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
Orderinstance all containedOrderIteminstances must be removed also.On attempt to remove
OrderIteminstance a check for existence of anyOrderinstance that referencesOrderItemmust be executed. If anyOrderinstance is found thenReferentialIntegrityViolationExceptionmust 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.ServicesandSession.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)
{}
}