Navigation

  • next
  • DataObjects.Net Manual »

Welcome to DataObjects.Net’s manual!

  • Introduction
    • What is DataObjects.Net?
    • DataObjects.Net Distribution
  • Getting started
    • Adding DataObjects.Net to your projects
    • Under the hood
  • Modelling domains
    • Persistent types
    • Entities
    • Structures
    • Fields
    • EntitySets
    • Associations
    • Indexes
    • Services
  • Domains
    • Domains overview
    • Configuring domain
    • Loading domain configuration
    • Building domain
    • Domain Events
  • Sessions
    • Sessions overview
    • Configuring sessions
    • Current session, session activation
    • Session Events
  • Keys
    • Keys overview
    • Working with keys
    • Key generators
  • Queries
    • Querying the data using LINQ
    • Base class library methods and operators in queries
    • EntitySet<T> operations
    • Projections to custom types
    • Local collections in queries
    • IQueryable<T> extensions
    • Compiled queries
    • IQueryable surface support map
    • Joins, casts, references, inheritance and subqueries in LINQ
    • Full-text queries
  • Transactions
    • Transactional transparency
    • Managing transactions
    • Nested transactions
  • Validation
    • Validation framework
    • Property constraints
  • Getting ideal performance
    • Statement batching
    • Lazy loading
    • Prefetch API
    • Delayed (future) queries
  • Database schema upgrade
    • Database schema upgrade overview
    • Upgrade handlers and hints
    • Recycled types and fields
    • Legacy database schema support
  • Advanced topics
    • Unified exceptions
    • Versions and concurrency control
    • Logging
    • Built-in services
    • Customizing LINQ translation
  • Extensions
    • Overview
    • Bulk Operations
    • Localization/Internationalization
    • Reprocessing
    • Security
    • Change Tracking/Auditing
    • Web
  • Epilogue

Navigation

  • next
© Copyright 2013, Xtensive LLC. Created using Sphinx 1.2b1.


  • previous |

Introduction

What is DataObjects.Net?

DataObjects.Net is a rapid database application development framework. It combines business logic layer (BLL) development library, object-relational mapper (ORM) and a set of storage providers allowing the same BLL code to work transparently on any of them.

Most frictionless environment you can find

DataObjects.Net helps you to model complex domains with ease. It supports the development path you used to follow. We put a lot of efforts to allow you thinking of persistent objects as of regular .NET objects. DataObjects.Net relies on “Model-First” (or “Code-First”) concept pushing developers to focus on domain model and its business logic instead of caring about how and when the entities must be persisted. This allows you to design models using just usual .NET classes. Moreover, we eliminated the need to maintain separate XML mapping files.

So building complex models containing hundreds of persistent types is nearly as simple as doing the same for non-persistent models.

Database schema: generation and evolution

DataObjects.Net provides built-in version-to-version migration pattern for your applications. It automatically creates, and, if necessary, upgrades the schema on each Domain startup preserving all the data. The upgrade process is fully customizable.

Perfect LINQ implementation

DataObjects.Net has a full-featured LINQ-to-SQL translator with support for multiple SQL dialects. It is, likely, the first ORM designed to compile LINQ queries not just to SQL, but to generally any other query language. Its internal query representation is similar to query plan used in SQL databases instead of AST. This feature allows it to perform really complex query transformations while producing query for target database. Currently DataObjects.Net supports:

  • Microsoft SQL Server 2005 / 2008 / 2012 (including Express editions)
  • Microsoft SQL Server CE 3.5
  • Windows Azure SQL Database
  • Oracle Database 10g and 11g (including Express editions)
  • PostgreSQL 8.3, 8.4, 9.0 and 9.1
  • Firebird 2.5
  • MySQL 5.1 and 5.5
  • SQLite 3

DataObjects.Net allows to transparently migrate between any of storages it supports.

High performance

DataObjects.Net provides various performance features:

  • DataObjects.Net supports so-called generalized batching. This means it almost never sends separate queries; instead, it joins the queries it produces in batches and sends them only when a result of some of these queries is necessary in your application. Of course, this is a rough description, but it explains what really happens there.
  • Future queries allows generalized batching to work almost ideally. DataObjects.Net batches future queries and CUD statements (insertions, updates and deletions) together.
  • Finally, there is prefetch API allowing to fetch arbitrary graphs of objects in few roundtrips.

System requirements

To install DataObjects.Net you need to have the following components:

  • Operating system: Windows Server 2008, Windows 7, Windows 8 with .NET Framework 3.5 SP1.
  • Development environment: optional; Visual Studio 2010 / 2012 / 2013 (including Express editions) or SharpDevelop.
  • Database server: optional; Any supported, SQL Server is recommended.

Note

Applications based on DataObjects.Net require just .NET Framework 3.5 SP1 or higher to be installed.

Supported programming languages

DataObjects.Net supports modern CLR-based languages:

  • C# (preferred, all examples in this manual are in C#)
  • Visual Basic.NET
  • F#

DataObjects.Net Distribution

NuGet

Each release of DataObjects.Net is distributed as set of NuGet packages. ORM core libraries, database providers and extensions are provided as separate items.

Core package

DataObjects.Net package includes core libraries and services as well as support for MS SQL Server and Windows Azure.

Database provider packages

  • DataObjects.Net provider for Firebird.
  • DataObjects.Net provider for MySQL.
  • DataObjects.Net provider for MS SQL Server CE 3.5.
  • DataObjects.Net provider for PostgreSQL.
  • DataObjects.Net provider for SQLite.
  • DataObjects.Net provider for Oracle. It requires Oracle Data Provider for .NET (ODP.NET) to be installed.

Extension packages

  • Bulk operations extension.
  • Localization extension.
  • Security extension.
  • Operation reprocessing extension.
  • ASP.NET support extension.
  • Logging provider for NLog.
  • Logging provider for log4net.
  • Change tracking extension.

Note

For all list of packages see Xtensive’s profile at NuGet gallery.

.zip package

As an alternative to NuGet channel DataObjects.Net can be downloaded as .zip package from <http://get.dataobjects.net>`_ official website.

Package contents

DataObjects.Net distribution consists of the following components:

  • Lib directory contains run-time libraries that are required for building and running DataObjects.Net-based applications. It has two subdirectories that contain binaries for .NET Framework 3.5 and 4.0 respectively.
  • Tools directory contains build-time components that are required for building DataObjects.Net-based applications.

Navigation

  • next
  • previous |


Getting started

Adding DataObjects.Net to your projects

  1. Open NuGet Package Manager and search for DataObjects.Net:

  2. Install DataObjects.Net package.

    The package will automatically install all dependencies and DataObjects.Net will be ready for use with MS SQL Server as a default storage.

  3. Install desired data providers (optional).

    In case you want to use DataObjects.Net with data provider other than MS SQL Server, install the desired provider from the list of DataObjects.Net packages.

  4. Install DataObjects.Net extensions (optional).

    Extensions are small projects that extend standard functionality of DataObjects.Net core.

Under the hood

For a project to be a valid DataObjects.Net project, it should meet some specific requirements. NuGet packages apply the required changes to project files on the fly during the installation.

Anyway, here are the requirements so you can get familiar with them:

  1. Project must reference Xtensive.Orm assembly and optionally a specific database provider (e.g. Xtensive.Orm.Oracle).

  2. Project should import DataObjects.Net.targets file. The file contains a bunch of MSBuild tasks that are executed during project build process.

    As NuGet packages are installed to a directory relative to the project directory, import directive usually looks like this;

    <Import Project=”..packagesXtensive.Orm.5.0.0DataObjects.Net.targets” />

    3.

Navigation

  •
          next
    •

  •
          previous |
    •

  • DataObjects.Net Manual »
    •

        © Copyright 2013, Xtensive LLC.
      Created using Sphinx 1.2b1.


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 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:

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:

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);
  3. 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.

  • 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:

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 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)
  {}
}


Domains

Domains overview

Domain is the primary storage access point. It is responsible for:

  • Building and maintaining storage-wide information, such as persistent model (like reflection in .NET, but specialized for persistent types), database model (tables, indexes and so on), mappings between them CLR types & properties and database entities (tables, columns, etc).

  • Validating and upgrading the database schema.

  • Creating Session objects, you can consider it is a Session factory as well.

    Note

    Domain objects can be used concurrently, as well as any other read-only content available from it (e.g. Domain.Model or Domain.ExtractedSchema).

To create a domain, you must create DomainConfiguration first. And there are several ways of doing this:

  1. You can create it manually in code
  2. Load it from App.config section
  3. Finally, you can load it from App.config and modify it

In our example we’ll use the 3rd way:

// Loading the configiration from App.config section
var config = DomainConfiguration.Load("Default");
// Modifying it by registering all the types from specified assembly
config.Types.Register(typeof(MyEntity).Assembly);
// And finally building the domain
var domain = Domain.Build(config);

Now we have the Domain object. But to start working with persistent entities, we need a Session instance.

Configuring domain

To configure and build a Domain, you can use DomainConfiguration class. It contains a set of options that could be used to customize DataObjects.Net domain-level behavior.

Connection to database

DataObjects.Net supports two alternative ways of providing connection information:

  • Using connection string name syntax. It allows to specify standard connection string for underlying ADO.NET provider. Using connection string is the recommended approach.
  • Using connection URL syntax. Its main advantage is unification (the same syntax for all supported databases). Connection URLs are similar to those used in DataObjects.Net 3.x.

Connection string syntax

For specifying connection information DataObjects.Net provides support for standard ADO.NET connection string syntax. You can pass provider name and connection string to DomainConfiguration constructor:

var config = new DomainConfiguration("sqlserver",
  "Data Source=localhost; Initial Catalog=Tests; " +
  "Integrated Security=True; MultipleActiveResultSets=true;");

Possible provider names are sqlserver (used for connecting to Windows Asure Database as well), sqlserverce, postgresql, oracle, postgresql, mysql, firebird and sqlite. Their canonical names can be found in Xtensive.Orm.WellKnown.Protocol class.

You can use connection strings in Domain configuration sections of App.config as well:

<domain
  name="Default"
  provider="sqlserver"
  connectionString="Data Source=localhost; Initial Catalog=MyDatabase;Integrated Security=True;"
  upgradeMode="Recreate" />

We recommend to use MultipleActiveResultTests parameter in connection string. Since DataObjects.Net supports on-demand materialization, this option allows it to deal with very large result sets. When this option is turned off, DO4 loads every result set completely. If connection URL is used instead of connection string, it is automatically added to underlying connection string.

ConnectionStrings.com provides many connection string examples for various ADO.NET providers.

Connection URL syntax

Connection URL can be passed to DomainConfiguration constructor or by assigning appropriate value to ConnectionUrl property:

var domainConfig = new DomainConfiguration("sqlserver://localhost/MyDatabase");

Alternatively you can specify connection URL in App.config section:

<configuration>
  <configSections>
    <section name="Xtensive.Orm" type="Xtensive.Orm.Configuration.Elements.ConfigurationSection, Xtensive.Orm" />
  </configSections>
  <Xtensive.Orm>
    <domains>
      <domain name="Default" connectionUrl="sqlserver://localhost/MyDatabase" />
    </domains>
  </Xtensive.Orm>
</configuration>

Any connection URL has the following structure:

protocol://user:password@server/database?parameter=value&parameter=value

Protocol is identical to provider name when using connection string. See previous section for details. Some parts of connection URL are optional, actual values and parameters may vary with each RDBMS. Any additional parameters are passed to underlying ADO.NET provider as is.

Microsoft SQL Server

  • sqlserver://localhost/Tests — connecting to database ‘Tests’ on a local machine with Windows authentication.
  • sqlserver://localhost\SQLExpress/WebSite — connecting to database ‘WebSite’ on a local machine with Windows authentication and non-default instance name ‘SQLExpress’.
  • sqlserver://iddqd:idfka@dbserver/Sales — connecting to database ‘Sales’ on a remote machine ‘dbserver’ with SQL Server authentication.

Azure SQL Database

  • sqlserver://login:password@server.database.windows.net/database — actual connection parameters could be taken from admin panel

Microsoft SQL Server CE

  • sqlserverce:///.\MyDatabase.sdf — using database file MyDatabase.sdf in current directory.
  • sqlserverce://:iddqd@/.\MyEncryptedDatabase.sdf — using password encrypted database file MyEncryptedDatabase.sdf in current directory. Note that special symbols ‘:’ and ‘@’ are required to conform with URL syntax.

PostgreSQL

  • postgresql://user:password@dbserver/tests — connecting to database ‘tests’ on server ‘dbserver’ as specified ‘user’ with specified ‘password’.
  • postgresql://user:password@dbserver/tests?Pooling=on&MinPoolSize=1&MaxPoolSize=5 — connecting to database ‘tests’ on server ‘dbserver’ as specified ‘user’ with specified ‘password’. Connection pooling is enabled.

Oracle

  • oracle://user:password@dbserver/tests – — connecting to database with service name ‘TESTS’ on server ‘dbserver’ as specified ‘user’ with specified ‘password’.
  • oracle://user:password@/sales — connecting to database SALES that is defined in TNSNAMES.ORA file.

Firebird

  • firebird://john:qqq@localhost/tests?Dialect=3&Connection lifetime=15&Pooling=true&MinPoolSize=0&MaxPoolSize=50&Packet Size=8192&ServerType=0 — connecting to database ‘tests’ as user ‘john’ with password ‘qqq’. Line breaks are added for formatting purposes.

SQLite

  • sqlite:///.\Tests.db3 — using database file Tests.db3 in current directory.

Default schema

By default DataObjects.Net maps all the entities to the default schema for the user specified in connection URL / string (authenticated user). If another schema must be used, you can specify it in DomainConfiguration.DefaultSchema or defaultScema attribute of domain element in App.config section.

Persistent types

Then you must register persistent types that will be used to build persistent model. Use Types collection and its Register method overloads to register single type, namespace or whole assembly. It is recommended to declare all persistent classes in separate namespace, e.g. MyCompany.MyProduct.Model and register all classes in this namespace.

domainConfig.Types.Register(
  typeof (Person).Assembly, typeof (Person).Namespace);

<domain
  name="Default"
  provider="sqlserver"
  connectionString="Data Source=localhost; Initial Catalog=MyDatabase; Integrated Security=True;"
  upgradeMode="Recreate">
    <types>
      <add assembly="MyCompany.MyProduct.Model" />
    </types>
</domain>

Persistent model can be also separated into several assemblies.

Upgrade mode

When domain is being built it analyzes existing database structure, and tries to upgrade it to actual model version. UpgradeMode property of DomainUpgradeMode type specifies how this upgrade should be performed. There are several available options:

  • Validate — Domain will check whether database schema is compatible with persistent model, anyway it will not be modified. If schema is incompatible, exception will be thrown.
  • Skip — No checks on database schema are performed. It is assumed to be compatible with current domain model.
  • Recreate — Database schema will be completely recreated, all existing data will be removed.
  • Perform — Storage upgrade will be performed. All missing database structures (columns, tables) will be added, excess ones will be removed. See database schema upgrade chapter for further details.
  • PerformSafely – The same as Perform mode, but any destructive operations (column or table removals, column type changes) require explicit declaration. If they are not explicitly declares exception is thrown before modifying database schema.
  • LegacyValidate and LegacySkip — legacy database compatibily mode with semantics similar to Validate and Skip respectively. See Legacy support section for more information.

Default upgrade mode value is PerformSafely.

domainConfig.UpgradeMode = DomainUpgradeMode.Recreate;

Naming conventions

NamingConvention property contains a set of rules for naming database structures such as tables, columns, indexes, etc. Several options are available.

NamingRules specifies underscore substitution for special symbols:

  • UnderscoreHyphens — replace all hypens with underscore symbol.
  • UnderscoreDots — replace all dots with underscore symbol.
  • Both options could be used.

By default dots and hypens are not underscored.

LetterCasePolicy specifies name transformation:

  • AsIs — no case transformations. This is default policy.
  • UpperCase — all identifiers are converted to upper case.
  • LowerCase — all identifiers are converted to lower case.

NamespacePolicy specifies namespace substitution for persistent entities:

  • Omit — omit namespace from table names. This is default option.
  • AsIs — namespaces are prepended to table names.
  • Hash — short namespace hashes are prepended to table names.
  • Synonymize — namespaces are replaced according to NamespaceSynonyms collection. You can provide your own prefix for each namespace. It would be prepended to table names. If namespace synonym is not specified for a particular namespace full name is used similar to AsIs mode.

Caching-related options

KeyCacheSize property defines size of domain-level key cache. Default value is 16384.

KeyGeneratorCacheSize property defines size of key generator cache. Default value is 128. Changing this property affects your database schema. See Keys chapter for details.

QueryCacheSize defines size of compiled query cache for LINQ queries. Default value is 256. This cache is managed by Last Recently Used (LRU) rule, so e.g. if its size is 100, 100 last recently used compiled queries won’t get recompiled on subsequent executions. See Compiled queries section for information about compiled LINQ queries.

RecordSetMappingCacheSize defines size of RSE mapping cache in entity materialization pipeline, default value is 1024. Each entry in this LRU cache stores mapping of a particular RecordSet to entities. Normally size of this cache must be proportional to count of types in the Domain, as well as to the QueryCacheSize.

Validation-related options

DataObjects.Net includes built-in validation framework, it’s switched on by default. If you don’t want to use built-in validation, set AutoValidation property to false.

You can also specify ValidationMode to use: Continuous or OnDemand. See Validation chapter for details.

Schema-related options

ForeignKeyMode specifies usage of foreign keys. Several options are available:

  • None — no foreign keys are generated.

    Reference — foreign keys are generated for reference fields.

    Hierarchy — foreign keys are generated for tables within ClassTable hierarchy. See Entities section for information about inheritance schemas.

    All — combines Reference and Hierarchy modes. This is default option.

If underlying database does not support foreign keys value of ForeignKeyMode is ignored. For the moment only SQLite provider does not support foreign keys. Even if None mode is specified DataObjects.Net still provides association handling features. See Associations section for futher information.

Collation property specifies collation for use in CREATE TABLE statements. This option should not be used for databases that allow setting collation at database-wide level. SQLite does not provide such setting thus Collation property could be used instead. DataObjects.Net provides several collations for use with SQLite provider. They are based on standard StringComparer class from .NET Framework:

  • StringComparer_Ordinal
  • StringComparer_Ordinal_IgnoreCase
  • StringComparer_CurrentCulture
  • StringComparer_CurrentCulture_IgnoreCase
  • StringComparer_InvariantCulture
  • StringComparer_InvariantCulture_IgnoreCase

The names should be self-explanatory.

Miscellaneous options

BuildInParallel property allows to enable parallel construction of a Domain. DataObjects.Net tries to minimize time for Domain.Build() by performing certain operations in separate thread. This option is enabled by default. If you for some reason don’t want separate thread to be used you could set this property to false.

ForcedServerVersion property allows to turn compatiblity with a particular version of database server. By default DataObjects.Net detects server version at start-up and uses recent features if they are available.

configuration.ForcedServerVersion = "9.0.0.0";

IncludeSqlInExceptions property allows to control message of exceptions that are thrown when error occurs when executing SQL query. By default query that cause the error is included into exception. You could disable such behavior by setting this property to false.

SchemaSyncExceptionFormat property allows to control message of exceptions that are thrown when database schema is not compatible with expect one. Two options are available:

  • Detailed — all relevant information is included. This is default option.
  • Brief — full schema difference is ommited. This enables behavior of DataObjects.Net 4.4 and earlier.

Loading domain configuration

Domain configuration can be also loaded from application configuration file. To do this you should add DataObjects.Net configuration section in your configuration file. One configuration section can contain several domain configurations, in the following example two domain configurations are declared (“stage” and “production”):

<configSections>
  <section
    name="Xtensive.Orm"
    type="Xtensive.Orm.Configuration.Elements.ConfigurationSection, Xtensive.Orm" />
</configSections>

<Xtensive.Orm>
  <domains>
    <domain name="stage"
            upgradeMode="Recreate"
            connectionUrl="sqlserver://localhost/MyStageDatabase">
      <types>
        <add assembly="MyAssembly" namespace="MyProduct.Model" />
      </types>
    </domain>
    <domain name="production"
            upgradeMode="Skip"
            connectionUrl="sqlserver://localhost/MyProductionDatabase">
      <types>
        <add assembly="MyAssembly" namespace="MyProduct.Model" />
      </types>
    </domain>
  </domains>
</Xtensive.Orm>

Domain configuration can be loaded from configuration file with the help of DomainConfiguration.Load() method:

var domainConfig = DomainConfiguration.Load("MyDomain");

Having that done you may change the configuration loaded setting any properties manually:

var domainConfig = DomainConfiguration.Load("default");
domainConfig.UpgradeMode = DomainUpgradeMode.Recreate;
domainConfig.Types.Register(typeof (Person).Assembly, typeof (Person).Namespace);

Building domain

When domain configuration is ready, we can build domain using Domain.Build() static method:

var domain = Domain.Build(domainConfig);

During the build process DataObjects.Net makes the following steps:

  • Builds domain model that includes all types registered in DomainConfiguration.Types registry and additional custom definitions.
  • Builds storage schema based on persistent model.
  • Extracts actual storage schema from database.
  • Upgrades database schema according to the DomainUpgradeMode specified in DomainConfiguration.UpgradeMode.

As a result this method will return built and ready to use Domain instance. Its configuration is accessible in read-only mode from Domain.Configuration property.

Domain Events

Domain type exposes 2 events:

  • EventHandler Disposing
    Occurs when Domain is about to be disposed.
  • EventHandler<SessionEventArgs> SessionOpen
    Occurs when new Session is opened and activated.

Usually SessionOpen event is used to subscribe to various Session events of every newly opened Session instance:

Domain.SessionOpen += (source, args) => {
  args.Session.Events.TransactionOpened += TransactionOpened;
  args.Session.Events.TransactionCommitting += TransactionCommitting;
  ...
};


Sessions

Sessions overview

The notion of Session is close to DataContext in ADO.NET Entity Framework.

Key facts about sessions:

  • Session is associated with single IDbConnection object that is used to access the underlying storage.
  • Session is created by Domain.OpenSession method. A session always belongs to the domain where it was created, and consequently allows to operate with (and only with) types/services, which are registered in that Domain.
  • Instances of persistent types always belong to (bound to) a session.
  • Session maintains its own cache of fetched entities, which is used and updated completely transparently.
  • Session maintains IdentityMap that provides entity uniqueness.
  • Session tracks changes in its entities and executes required insert, modify & remove operations on Persist.
  • Sessions aren’t safe for the multithreaded operations. To work with a storage in multiple threads you should use separate session for each thread, or manually synchronize access to them.

Session implements IDisposable interface so it is convenient to use it within using construct.

using (var session = domain.OpenSession()) {

  // Work with persistent classes here
}

Session profiles

Starting from DataObjects.Net 4.4 ‘Automatic Session activation’ and ‘Automatic Transaction opening’ modes made optional, moreover, they are switched off by default. In addition, Session Profile notion is introduced. It represents a combination of predefined session options for particular scenarios.

For now there are three main profiles: Server profile, Client profile & Legacy profile.

  • Server profile is intended to be used in server scenarios, such as ASP.NET, ASP.NET MVC, WCF Data Services, etc. It doesn’t include ‘Automatic Session activation’ and ‘Automatic Transaction opening’ modes, so several Session instances could be used within a thread and all actions with Entities, Structures & EntitySets require the presence of opened Transaction. This profile is the default one.
  • Client profile is designed for WPF, Windows Forms, Console applications, whatever. It uses the new ‘Disconnected’ Session mode which means that Session encapsulates an instance of DisconnectedState and runs in occasionally connected conditions, logs modifications of Entities and applies them to a database on request. It doesn’t include ‘Automatic Session activation’ either but has ‘Automatic Transaction opening’ option switched on (in this mode it uses local in-memory transactions, not database ones, in order to log Entity modifications).
  • To preserve compatibility with Session & Transaction behavior from previous version of DataObjects.Net a special profile named Legacy is added, which has both ‘Automatic Session activation’ and ‘Automatic Transaction opening’ modes switched on.

Session profiles usage

Session profile is a combination of SessionOptions values, so we build SessionConfiguration instance using this profile and construct Session afterwards.

var configuration = new SessionConfiguration(
    "mySession", SessionOptions.ClientProfile);
using (var session = domain.OpenSession(configuration)) {
   // some code
}

In case you want every Session within your Domain runs in a particular profile by default, you can set up Default Session in applicatin configuration file:

<domain upgradeMode="..." connectionUrl="...">
  <types>
    <add assembly="..."/>
  </types>
  <sessions>
    <session name="Default" options="ClientProfile" />
  </sessions>
</domain>

You may achieve the same by setting up Default Session in C# code:

var domainConfiguration = new DomainConfiguration();
var sessionConfiguration = new SessionConfiguration(
    "Default", SessionOptions.ClientProfile);
domainConfiguration.Sessions.Add(sessionConfiguration);
var domain = Domain.Build(domainConfiguration);

Configuring sessions

Another method overload domain.OpenSession(sessionConfig) accepts SessionConfiguration instance, that contains several session-level options:

  • CacheSize – size of the entity cache, default value is 16384 entities.
  • CacheType – LruWeak (default value) or Infinite.
  • DefaultIsolationLevel for transactions, isolation level can be also specified when a particular transaction is being opened.
  • BatchSize – maximal size of SQL statement batches, this affects create, update, delete operations and future queries.
  • Options – session options flag.

Session configuration can be created manually:

var sessionCongfig = new SessionConfiguration {
  BatchSize = 25,
  DefaultIsolationLevel = IsolationLevel.ReadCommitted,
  CacheSize = 1000,
  Options = SessionOptions.ServerProfile
};

Or declared and loaded from configuration file:

<domain name="TestDomain"
        upgradeMode="Recreate"
        connectionUrl="sqlserver://localhost/MyTests" >
  <sessions>
    <session name="TestSession"
             batchSize="25" isolationLevel="ReadCommitted"
             cacheSize="1000" options="AutoShortenTransactions" />
  </sessions>
</domain>

var domainConfig = DomainConfiguration.Load("TestDomain");
var sessionConfig = domainConfig.Sessions["TestSession"];

When session configuration is created or loaded, session with this configuration can be opened by special Open() method overload:

using (var session = domain.OpenSession(sessionConfig)) {
  // ...
}

Current session, session activation

Note

This chapter describes session particularities in DataObjects.Net 4.3 and DataObjects.Net 4.4 with SessionProfile=Legacy.

Session class implements IContext interface, it means that a session can be either active or not active in particular thread. Each thread can contain only one active session, that can be a accessed via Session.Current property or Session.Demand() method. When you creates new entitiy or loads it from database, they are automatically bounded to the current session.

New session can be opened and activated by domain.OpenSession() method. In the following example newly opened session is automatically activated within the using block:

using (var session = domain.OpenSession()) {

  // creating new entity
  var newPerson = new Person();

  // fetching entity by its ID
  var fetchedPerson = session.Query.Single<Person>(personId);

  Console.WriteLine("Our session is current: {0}",
    Session.Current==session);

  Console.WriteLine("New entity is bound to our session: {0}",
    newPerson.Session==session);

  Console.WriteLine("Fetched entity is bound to our session: {0}",
    fetchedPerson.Session==session);
}

The result will be true in all cases.

Existing session can be activated by Activate() method:

using (session.Activate()) {

  // Work with persistent classes here
}

DataObjects.Net provides the following session activation pattern:

  • Session.Activate binds a session to the current thread.
  • Session.Deactivate unbinds currently active session from the current thread.
  • Active (or current) session is session that was bound to the current thread most recently, but not yet deactivated.
  • All above implies each thread maintains its own stack of activated sessions. When activation happens on a particular thread, a new session is added to the top of its stack; this session becomes active (current) in this thread. When a particular session is deactivated, the session below it on the stack becomes active (current). If deactivated session is not the topmost session on the stack, all the above sessions are deactivated implicitely.

Session activation API:

  • Session.Current returns active (current) Session bound to the current Thread; if there is no active session, it returns null.
  • Session.Demand() returns active session, if it exists. If there is no active session, it throws InvalidOperationException.
  • Session.Activate() activates the session. This method returns IDisposable object that must be disposed to deactivate the session. So normally Session.Activate() must be used inside using block.

That’s true:

  • Moreover, by default any public method (or property accessor) of any SessionBound descendant is decorated by aspect activating its SessionBound.Session for the duration of execution of this method.
    • [Transactional(ActivateSession = false)] attribute prevents automatic session activation for public SessionBound methods;
    • [Transactional(ActivateSession = true)] enforces automatic session activation for any (e.g. non-public) SessionBound methods.

So since all the objects requiring a Session (e.g. Entity, Structure and EntitySet<T>) are SessionBound descendants, almost all of their public methods implicitly activate the Session they’re bound to. This ensures:

  • You can use Session.Demand() and Session.Current inside their code, or inside any code they invoke.

  • So e.g. you can invoke static methods without passing the Session to them directly.

    Note

    Is the session activation technique fast? For example, Session.Activate() always returns an IDisposable object – so does this mean any SessionBound method call allocates something on heap?

No, it doesn’t: actually, Session.Activate() returns a special IDisposable singleton (so-called void scope) doing nothing on its disposal, if this Session is already active. So this is pretty fast process.

Current Domain

Presence of current Session allows us to provide current Domain as well:

  • Domain.Current is actually based on Session.Current;
  • The same is true for Domain.Demand().

Here is actual Domain.Current property code:

public static Domain Current {
  get {
    var session = Session.Current;
    return session!=null ? session.Domain : null;
  }
}

Session Events

Session type exposes wide range of events, from Entity-related events to DbCommand events. Session events are accessed through Session.Events property. Unlike ‘event-like’ protected virtual methods in Entity & EntitySet types, session events are preferred in case you don’t have access to source code of the domain model or going to be less invasive and more flexible.

Session-related events

  • EventHandler<QueryEventArgs> QueryExecuting
    Occurs when LINQ query is about to execute.
  • EventHandler<QueryEventArgs> QueryExecuted
    Occures when LINQ query is executed, but before enumeration of result.
  • EventHandler Disposing
    Occurs when Session is about to be disposed.
  • EventHandler Persisting
    Occurs when Session is about to Orm.Session.SaveChanges changes.
  • EventHandler Persisted
    Occurs when Session persisted.

Entity-related events

  • EventHandler<EntityEventArgs> EntityMaterialized
    Occurs when Entity is materialized.
  • EventHandler<EntityEventArgs> EntityCreated
    Occurs when Entity is created.
  • EventHandler<EntityEventArgs> EntityChanging
    Occurs when Entity is about to change.
  • EventHandler<EntityVersionInfoChangedEventArgs> EntityVersionInfoChanging
    Occurs when Entity.Entity.VersionInfo is about to change.
  • EventHandler<EntityVersionInfoChangedEventArgs> EntityVersionInfoChanged
    Occurs when Entity.Entity.VersionInfo is changed.
  • EventHandler<EntityFieldEventArgs> EntityFieldValueGetting
    Occurs when field value is about to be read.
  • EventHandler<EntityFieldValueEventArgs> EntityFieldValueGet
    Occurs when field value was read successfully.
  • EventHandler<EntityFieldValueGetCompletedEventArgs> EntityFieldValueGetCompleted
    Occurs when field value reading is completed.
  • EventHandler<EntityFieldValueEventArgs> EntityFieldValueSettingAttempt
    Occurs when is 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).
  • EventHandler<EntityFieldValueEventArgs> EntityFieldValueSetting
    Occurs when is 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).
  • EventHandler<EntityFieldValueSetEventArgs> EntityFieldValueSet
    Occurs when field value was changed successfully.
  • EventHandler<EntityFieldValueSetCompletedEventArgs> EntityFieldValueSetCompleted
    Occurs when field value changing is completed.
  • EventHandler<EntityEventArgs> EntityRemoving
    Occurs when Entity is about to remove.
  • EventHandler<EntityEventArgs> EntityRemove
    Occurs when Entity removed.
  • EventHandler<EntityRemoveCompletedEventArgs> EntityRemoveCompleted
    Occurs when Entity removing is completed.

EntitySet-related events

  • EventHandler<EntitySetEventArgs> EntitySetChanging
    Occurs when EntitySetBase is about to change.
  • EventHandler<EntitySetItemEventArgs> EntitySetItemRemoving
    Occurs when EntitySetBase item is about to remove.
  • EventHandler<EntitySetItemEventArgs> EntitySetItemRemove
    Occurs when EntitySetBase item removed.
  • EventHandler<EntitySetItemActionCompletedEventArgs> EntitySetItemRemoveCompleted
    Occurs when EntitySetBase item removing is completed.
  • EventHandler<EntitySetItemEventArgs> EntitySetItemAdding
    Occurs when EntitySetBase item is about to remove.
  • EventHandler<EntitySetItemEventArgs> EntitySetItemAdd
    Occurs when EntitySetBase item removed.
  • EventHandler<EntitySetItemActionCompletedEventArgs> EntitySetItemAddCompleted
    Occurs when EntitySetBase item removing is completed.
  • EventHandler<EntitySetEventArgs> EntitySetClearing
    Occurs when EntitySetBase is about to be cleared.
  • EventHandler<EntitySetEventArgs> EntitySetClear
    Occurs when EntitySetBase is cleared.
  • EventHandler<EntitySetActionCompletedEventArgs> EntitySetClearCompleted
    Occurs when EntitySetBase clearing is completed.

Transaction-related events

  • EventHandler<TransactionEventArgs> TransactionOpening
    Occurs when Transaction is about to be opened.
  • EventHandler<TransactionEventArgs> TransactionOpened
    Occurs when Transaction is opened.
  • EventHandler<TransactionEventArgs> TransactionPrecommitting
    Occurs when Transaction is about to be committed.
  • EventHandler<TransactionEventArgs> TransactionCommitting
    Occurs when Transaction is about to be committed.
  • EventHandler<TransactionEventArgs> TransactionCommitted
    Occurs when Transaction is committed.
  • EventHandler<TransactionEventArgs> TransactionRollbacking
    Occurs when Transaction is about to be rolled back.
  • EventHandler<TransactionEventArgs> TransactionRollbacked
    Occurs when Transaction is rolled back.

Misc events

  • EventHandler<DbCommandEventArgs> DbCommandExecuting
    Occurs when DbCommand is about to execute.
  • EventHandler<DbCommandEventArgs> DbCommandExecuted
    Occurs when DbCommand is executed.
  • EventHandler<KeyEventArgs> KeyGenerated
    Occurs when local Key created.


Keys

Keys overview

Key represents a set of identity fields. Every hierarchy defines its own identity structure and every entity is identified by key. Number of fields in key is limited to 8. DataObjects.Net supports the following .NET types to be used in identity fields:

  • boolean;
  • byte, sbyte, short, ushort, int, uint, long, ulong;
  • string, char;
  • double, float, decimal;
  • Guid, DateTime, TimeSpan;
  • Reference to Entity

Usage of Structure and EntitySet<T> types is not allowed.

Number and types of identity fields are set once for all persistent hierarchy, so all descendants of hierarchy root share the key structure. In order to set up identity fields, KeyAttribute class should be used:

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

  ...
}

KeyAttribute must be placed on each identity field. Also note, that identity field must be immutable, it is prohibited for identity field to have a setter other than private.

For persistent types with complex keys (which have more than one identity field) explicit identity field order is strictly recommended, because the particular order of list of properties, got with the help of .NET reflection, is not guaranteed at all.

[HierarchyRoot]
public class BookReview : Entity
{
  [Field, Key(0)]
  public Book Book { get; private set; }

  [Field, Key(1)]
  public Person Reviewer { get; private set; }

  [Field(Length = 4096)]
  public string Text { get; set; }

  public BookReview(Session session, Book book, Person reviewer)
    : base(session, book, reviewer)
  {}
}

Pay attention to KeyAttribute usage. In this example it also used to set the position of identity field within complex key. So for BookReview type the structure of key is {Book, Person}. Also note that values for both identity fields are required in BookReview constructor and passed to the base constructor of Entity where actual key is constructed.

Working with keys

Creating keys

Creating the next unique key in corresponding key sequence for the specified persistent type. This can be done by these group of static methods:

Key.Generate<Document>(Session);
// or
Key.Generate(Session, typeof (Document));

Note that methods don’t receive any arguments except Session & Type. This means that we ask to generate the next unique key in key sequence.

It is also worth noting that generation of next key in sequence requires an instanse of Session because DataObjects.Net might need to connect to underlying storage and call the corresponding key generator for a next key in sequence.

For example, to fetch an entity from storage we need a key that identifies it. In this case we need to construct a key with already known value(s).

// Active session is not required
Key.Create<Document>(Domain, params object[] values);
Key.Create(Domain, typeof (Document), params object[] values);

If we want to build a key for an instance of Document class with identifier equals to 25 we write something like this:

var key = Key.Create<Document>(Domain, 25);

Note that presence of Session is not required as we don’t need a connection to underlying storage at all.

There is no public constructor in Key class. The reason for this and the usage of Factory method pattern instead of constructors is that we have several Key implementations, 4 of them are designed and extremely optimized for short keys (Key<T1>, Key<T1,T2>, Key<T1,T2,T3>, Key<T1,T2,T3,T4>) and one is for keys with length up to 8 fields (LongKey).

Serializing and deserializing keys

Keys can be automatically serialized into the corresponding string representation with the help of Key.Format() method and deserialized from the string using static Key.Parse() method.

var key = Key.Create<Document>(25);
var str = key.Format();
Console.WriteLine(str);
// This will print: "103:25"
// where 103 is identifier of Document type in Domain model
// and 25 is the value of identifier field.

var key2 = Key.Parse(Domain, str);
Assert.AreEqual(key, key2);

Key generators

Where keys come from?

Every entity is uniquely identified by key and Entity.Key property is immutable during whole entity’s lifecycle. The only moment when Entity.Key property can be set is the moment of entity construction. There are two scenarios and DataObjects.Net supports both:

1. Values of identity fields are provided by user code (not by framework). User code is responsible for passing identity values directly to entity constructor where these values are automatically transformed into key.

[HierarchyRoot]
[KeyGenerator(KeyGeneratorKind.None)]
public class Book : Entity
{
  [Field, Key]
  public string ISBN { get; private set; }

  // Accepts identity value (ISBN) and passes it to the base Entity constructor
  public Book(Session session, string isbn)
    : base(session, isbn)
  {
  }
}

2. Values of identity fields are provided by framework. This scenario might involve usage of database identity generators, tables with auto-increment column as well as any custom identity generators in order to produce new identity values for entity and build its key.

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

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

While the first scenario is straighforward and doesn’t need any framework participation at all (except creating key from values passed to constructor), the second one is not so obvious because it requires framework to know how to obtain the next unique identity values for the specified persistent type. To solve this task the concept of key generator was introduced in DataObjects.Net.

Standard key generators

Key generator is a special object that knows all necessary information concerning generation of unique keys for particular hierarchy of persistent types, including:

  • details of identity generator implementation;
  • structure of key;
  • size of key cache, if generator supports caching;
  • mapping name, if generator is mapped to a particular database entity;
  • etc.

DataObjects.Net contains key generator implementations for the following types of identity fields:

  • byte, sbyte, short, ushort, int, uint, long, ulong;
  • string, Guid;

They can be used for persistent types with one identity field. This means they are not suitable for persistent types with complex key (more than one identity field). Sequential key generators (for integer types) also support caching.

Depending on features of the underlying storage, sequential key generator is mapped to sequence or a standalone table with one autoincrement column. Usually, the name of such physical entity is produced according to the rule ‘{type}-Generator’. For example, sequence for key generator of Int32 type will be ‘Int32-Generator’.

Shared key generators

In order to increase overall key generator performance and to widen persistent interfaces support single key generator is shared between all hierarchies it can serve. In other words, by default all hierarchies which have identical key structure are served by the same key generator.

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

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

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

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

Here we have 2 hierarchies with identical key structure (one field type of int) and no custom key generator is specified, so the default key generator is used. Having that, only 1 key generator (‘Int32-Generator’, if you look into database) will be created and it will serve both hierarchies at once. This means that sequence of Book identifiers as well as sequence Author identifiers will be shared. Say, one can get 1,2,3,6 for Book identifiers and 4,5,7,8,9 for Author identifiers. Nevertheless, all keys produced by this key generator are unique and can be used in any number of hierarchies which can be served by this key generator type without any restrictions.

Dedicated key generators

Dedicated key generators are the opposite case to shared generators. Having dedicated key generators, you get separate sequence for a given hierarchy.

[HierarchyRoot]
[KeyGenerator(KeyGeneratorKind.Default, Name = "BookGenerator")]
public class Book : Entity
{
  [Field, Key]
  public int Id { get; private set; }

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

[HierarchyRoot]
[KeyGenerator(KeyGeneratorKind.Default, Name = "AuthorGenerator")]
public class Author : Entity
{
  [Field, Key]
  public int Id { get; private set; }

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

In this example we indicate, we want to have 2 separate key generators: ‘BookGenerator’ and ‘AuthorGenerator’. DataObjects.Net will create them as two standalone sequences or tables with autoincrement field. Each hierarchy will be served by dedicated key generator with the corresponding name.

Hierarchies without key generator

In scenarios when key generator for a hierarchy is not required at all, this must be set up appropriately:

[HierarchyRoot]
[KeyGenerator(KeyGeneratorKind.None)]
public class Book : Entity
{
  [Field, Key]
  public string ISBN { get; private set; }

  // Accepts identity value (ISBN) and passes it to the base Entity constructor
  public Book(Session session, string isbn)
    : base(session, isbn) { }
}

Pay attention to KeyGeneratorAttribute usage together with identity values that are provided from the outside and passed to constructor.

Custom key generators

DataObjects.Net supports wide variety of keys and provides built-in key generator implementation only for keys that contain one identity field. For other scenarios that require key generator it is suggested to define custom key generator, inheriting from KeyGenerator type and implement a pair of its methods.

Here is the sample implementation of key generator for one-column Guid key:

[Service(typeof (KeyGenerator), Name = "MyGuidKeyGenerator")]
public class MyGuidKeyGenerator : KeyGenerator
{
  public override void Initialize(Domain domain, TupleDescriptor keyDescriptor)
  {
    // Key generator initialization logic, if any.
    // Executed on Domain.Build() step
  }

  public override Tuple GenerateKey(KeyInfo keyInfo, Session session)
  {
    // Creating tuple for key
    var keyTuple = Tuple.Create(keyInfo.TupleDescriptor);
    // Setting the first column of tuple with generated guid value
    keyTuple.SetValue(0, Guid.NewGuid());
    return keyTuple;
  }
}

The name that set in ServiceAttribute is uniquelly identifies custom key generator.

In order to indicate that a hierarchy must be served with custom key generator, use KeyGeneratorAttribute. Sample usage:

[HierarchyRoot]
[KeyGenerator(KeyGeneratorKind.Custom, Name = "MyGuidKeyGenerator")]
public class Author : Entity
{
  [Field, Key]
  public Guid Id { get; private set; }

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

Custom generators are found by their names, so it is necessary to set the KeyGeneratorAttribute.Name property with correct name of key gerenator you defined earlier.


Queries

Querying the data using LINQ

LINQ is the standard on querying the data on .NET platform. DataObjects.Net provides full-featured LINQ API for querying persistent objects.

Single endpoint: Session.Query property

Session.Query property provides a set of members allowing you to query the database using LINQ.

Session.Query.All<T>() method is the most important one. It creates and returns a new IQueryable<T> instance that allows you to get all entities assignable to type T.

var products = session.Query.All<Product>();
foreach (var product in products) {
  ...
}

**Note**

In this chapter we'll often use term *query* as a synonym for
``IQueryable`` instance.

Unless session uses client profile you’ll need an active transaction to enumerate query results.

using (var session = domain.OpenSession()) {
  using (var t = session.OpenTransaction()) {
    var articles = from a in session.Query.All<Article>() select a;
    foreach (var article in articles) {
    ...
    }
  }
}

Each query is implicitly bound to a session that created it. To enumerate query result this session should not be disposed at the moment of query execution. The following example demostrates incorrect usage of query.

IQueryable<Product> products;
using (var session = domain.OpenSession()) {
  using (var t = session.OpenTransaction()) {
    products = session.Query.All<Product>();
  }
}
// Wrong! Session that created products queryable is already disposed here.
foreach (var p in products) {
}

The object returned by Session.Query.All() method implements IQueryable interface. This means you could use standard LINQ facilities to construct more complex queries.

var result =
  from e in session.Query.All<Employee>()
  where e.FullName!=null
  select e;

from p in session.Query.All<Product>()
select p.UnitsInStock * p.UnitPrice

Multiple IQueryable<T> instances could be used in the same query expression:

var result =
  from c in session.Query.All<Customer>()
  join o in session.Query.All<Order>()
    on c equals o.Customer into ords
  join e in session.Query.All<Employee>()
    on c.Address.City equals e.Address.City into emps
  select new {ords = ords.Count(), emps = emps.Count()};

Fetching a single entity

Session.Query provides two method groups allowing fetch a single entity:

  • Single(...)
  • SingleOrDefault(...)

These methods are pseudo-LINQ. They generally follow semantics of the standard Single and SingleOrDefault extension methods. However some differences exist. Regular LINQ queries always perform a database query. Pseudo-LINQ methods first try to find entity in session cache. If entity is found there it is returned immediately. Otherwise database query is performed.

Base class library methods and operators in queries

DataObjects.Net allows using many of base class library operators and methods in queries. Generally you can expect most of the members of the supported persistent types to work. Please see persistent fields section for information about supported types. All provided members are supported only for the specified types.

  • Standard C# operators: == != < <= > >= || && ! ?? ?: + - * /

    IQueryable<Product> calculableFilter =
  session.Query.All<Product>()
    .Where(p => 100 + p.UnitPrice * p.UnitsInStock/100 >= 100);

IQueryable<decimal> calculableColumn =
  session.Query.All<Product>()
    .Select(p => 100 + p.UnitPrice * p.UnitsInStock/100);

    IQueryable<Product> calculableFilter =
  session.Query.All<Product>().Where(p =>
    p.UnitPrice >= 100 && p.UnitPrice<=200 || !(p.ProductName=="Name"));

IQueryable<bool> calculableColumn =
  session.Query.All<Product>().Select(p =>
    p.UnitPrice >= 100 && p.UnitPrice<=200 || !(p.ProductName=="Name"));

  • Standard methods: Equals, ToString, IEquatable<T>.Equals, IComparable<T>.CompareTo.

  • Decimal methods: Add, Compare, Divide, Parse, Remainder, Subtract

  • Math type methods:

    • Arithmetics: Abs, Sign, Min, Max, BigMul, Exp, Log, Log10, PI, Pow, Sqrt
    • Roundings: Floor, Ceiling, Round, Truncate
    • Trigonometrics: Acos, Asin, Atan, Atan2, Sinh, Tan, Tanh, Cos, Cosh

  • Nullable<T> properties: Value, HasValue

  • String methods:

    • Methods: StartsWith, EndsWith, Contains, Substring, ToUpper, ToLower, Trim, TrimStart, TrimEnd, Length, ToString, Replace, Insert, Remove, IsNullOrEmpty, Concat, PadLeft, PadRight, Compare, CompareTo, IndexOf, Chars, Contains
    • Operators: [] +
    • StringExtensions extension methods: LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual

  • DateTime:

    • Extractors: Year, Month, Day, Hour, Minute, Second, Millisecond, TimeOfDay, Date, DayOfWeek, DayOfYear
    • Constructors: new DateTime(year, month, day), new DateTime(year, month, day, hour, minute, second, millisecond)
    • Methods: Add, Subtract, Now, Today, IsLeapYear, DaysInMonth,

  • TimeSpan:

    • Constructors: new TimeSpan(days, hours, minutes, seconds, milliseconds), new TimeSpan(ticks), new TimeSpan(hours, minutes, seconds), new TimeSpan(days, hours, minutes, seconds)
    • Static construction methods: FromDays, FromHours, FromMinutes, FromSeconds, FromMilliseconds, FromTicks
    • Extractors: Milliseconds, Seconds, Minutes, Hours, Days
    • Converters: Ticks, TotalMilliseconds, TotalSeconds, TotalMinutes, TotalHours, TotalDays
    • Methods: Add, Subtract, Negate, Duration

EntitySet<T> operations

EntitySet<T> implements IQueryable<T>, so it is possible to use it as query root:

Customer customer = GetCustomer();
EntitySet<Order> orders = customer.Orders;
var result =
  from order in orders
  where order.Freight > 100
  select order;

GroupBy method returns IQueryable<IGrouping<TGroupingKey, TElement>>. Each IGrouping<TGroupingKey, TElement> implements IQueryable<TElement> as well, so it can be used as query root after explicit cast to IQueryable<TElement>. An alternative to explicit cast here is standard AsQueryable method – if source is queryable, it simply makes the cast we need:

IQueryable<IGrouping<decimal, Product>> groupings =
  from product in session.Query.All<Product>()
  group product by product.UnitPrice;

foreach (IGrouping<decimal, Product> grouping in groupings) {
  Log.Info("Unit Price {0}", grouping.Key);

  // Use IGrouping<TGroupingKey, TElement> as IQueryable<TElement>
  var categories =
    from product in grouping.AsQueryable()
    select product.Category.CategoryName;

  foreach (string category in categories)
    Log.Info("Category {0}", category);
}

Subquery result implements IQueryable<T> as well:

IQueryable<IQueryable<Order>> result =
  from customer in session.Query.All<Customer>()
  select // Create subquery
    from order in session.Query.All<Order>()
    where order.Customer==customer
    select order;

foreach (IQueryable<Order> orders in result) {
  var subQueryCount = orders.Count(); // Execute subquery.
  // ....
}

Projections to custom types

DataObjects.Net can materialize not just the objects of built-in types (Entity, Structure, etc.), but objects of generally any type:

var orders = from customer in session.Query.All<Customer>()
select new CustomOrder(customer.Id) {
  Country = customer.Address.Country,
  City = customer.Address.City
};

As you see, here we create a custom, non-persistent CustomerOrder type and set its two properties – in fact, by doing this we’re mapping these properties to data participating in query. DataObjects.Net maintains such mapping information for the whole query, so it is possible to use properties of custom materialized objects further in the query:

var filteredOrders =
  from order in
    from customer in session.Query.All<Customer>()
    select new CustomOrder(customer.Id) {
      Country = customer.Address.Country,
      City = customer.Address.City
    }
  where order.Country=="Russia"
  select order;

Local collections in queries

DataObjects.Net allow you to mix operations on local collections and server-side data in queries. Local collections are objects of IEnumerable<T>. While executing such a query, DataObjects.Net either creates a temporary table, persists the data extracted from local objects into it and uses it in SQL statement as any other table, or injects local collection content into generated SQL (e.g. into in IN (...)). The last case is used if collection is small enough and usage of IN or boolean expression with collection elements is possible.

// Local collection of decimals.
var freights = new List<decimal> {1, 234, 334, 554, 343, 753};

var result = from storageOrders in session.Query.All<Order>()
join freight in freights on storageOrders.Freight equals freight
select storageOrders;

Moreover, DataObjects.Net provides Session.Query.Store<T> method – it converts IEnumerable<T> to IQueryable<T>, so it allows use local collections as query roots. Such queryables support all the operations you can do with any other DataObjects.Net queryable.

As it was mentioned, underlying IEnumerable<T> will be persisted to storage as temporary table on execution of such query.

var freights = new List<decimal> {1, 234, 554, 343};
var localCollectionQuery = session.Query.Store<decimal>(freights);

// It's possible to use params T[]:
// var localCollectionQuery = session.Query.Store<decimal>(1, 234, 554, 343);

var result = from freight in localCollectionQuery
join storageOrders in session.Query.All<Order>()
  on freight equals storageOrders.Freight
select storageOrders;

IQueryable<T> extensions

DataObjects.Net extends IQueryable<T> type with a set of useful methods:

LeftJoin simplifies writing of left joins. The syntax is exactly the same as of standard Join method, the only difference is the way query is executed.

var territories = session.Query.All<Territory>();
var regions = session.Query.All<Region>();
var result = territories.LeftJoin(
  regions,
  territory => territory.Region.Id,
  region => region.Id,
  (territory, region) => new {
    territory.TerritoryDescription,
    region.RegionDescription
  });

Note that there is another way to write left join using standard LINQ syntax:

var result =
   from order in session.Query.All<Order>()
  join customer in session.Query.All<Customer>()
    on order.Customer equals customer into oc
  from joinedCustomer in oc.DefaultIfEmpty()
  select new {
    CustomerId = joinedCustomer.Id,
    joinedCustomer.CompanyName,
    joinedCustomer.Address.Country
  };

DataObjects.Net LINQ translator recognizes this way. But we decided to add LeftJoin to make the syntax more explicit.

Lock adds locking hints to the underlying query:

var customers = session.Query.All<Customer>()
 .Where(c => c.Key == key)
 .Lock(LockMode.Update, LockBehavior.Skip);

In checks if value is contained in specified list of values. In fact, it is an alternative syntax for Contains. The following two queries are equal:

var customers = from c in session.Query.All<Customer>()
where c.Id.In("FISSA", "PARIS")
select c.Orders;

var customers = from c in session.Query.All<Customer>()
where new List<string> {"FISSA", "PARIS"}.Contains(c.Id)
select c.Orders;

DataObjects.Net extension methods

Compiled queries

Query compilation takes time. If performance is really important, it is possible to compile the query once and execute it multiple times with different parameters afterward. This could decrease average execution time of a query by up to 10 times.

Session.Query.Execute method allows to compile and run such compiled query:

private IEnumerable<Employee> GetEmployees(DateTime date)
{
  return session.Query.Execute(q =>
    from employee in q.All<Employee>()
    where employee.BirthDate < date
    select employee);
}

Only the first call to GetEmployees method will cause query compilation. Subsequent execution of this method will lead to execution of previously compiled query, but with new parameters.

Unfortunately, LINQ has some limitation related to Take, Skip, ElementAt, ElementAtOrDefault in such scenarios: all of them allow to pass only an integer number as parameter, but not an expression. This number is always “encoded” as constant in LINQ expression our translator gets, thus there is no chance to change the value after the query has been compiled.

But DataObjects.Net provides its own Take, Skip, ElementAt, ElementAtOrDefault extension methods with parameter of type Expression<Func<int>> instead of int. This solution allows to work around this issue.

The first method of the following example throws an exception, but the second one works properly.

private IEnumerable<Customer> TakeCustomersIncorrect(int amount)
{
  return session.Query.Execute(q => q.All<Customer>().Take(amount));
}

private IEnumerable<Customer> TakeCustomersCorrect(int amount)
{
  return session.Query.Execute(q => q.All<Customer>().Take(() => amount));
}

IQueryable surface support map

The following IQueryable extension methods are fully supported by DataObjects.Net LINQ translator except the overloads that take IComparer or IEqualityComparer as an argument:

All Any AsQueryable Average Concat Contains Count Distinct
Except First FirstOrDefault GroupBy GroupJoin Intersect Join
LongCount Max Min OfType OrderBy OrderByDescending Reverse
Select SelectMany Single SingleOrDefault Sum
ThenBy ThenByDescending Union Where

Paging operations (ElementAt ElementAtOrDefault Take Skip) are supported in regular queries only. These methods are not supported in compiled queries. DataObjects.Net-specific overloads that work in compiled queries are available. These overloads require Func<int> argument instead of int and could be used in regular queries as well as compiled queries.

DefaultIfEmpty is supported in special scenario when it is used with SelectMany:

from customer in session.Query.All<Customer>()
from order in session.Query.All<Order>()
  .Where(o => o.Customer==customer)
  .DefaultIfEmpty()
select {customer, order}

DataObjects.Net does not support any LINQ method that takes IComparer or IEqualityComparer as an argument. Also the following methods are not supported:

Aggregate Cast TakeWhile SkipWhile SequenceEqual Last LastOrDefault

Joins, casts, references, inheritance and subqueries in LINQ

Joins

LINQ provides standard Join extension method that normally translates to SQL as inner join. The following query:

var result =
  from product in session.Query.All<Product>()
  join supplier in session.Query.All<Supplier>() on product.Supplier.Id
    equals supplier.Id
  select new {product.ProductName, supplier.ContactName, supplier.Phone};

will be translated to the following SQL:

SELECT [a].[ProductId],
       [a].[TypeId],
       [a].[ProductName],
       [b].[SupplierId]  AS [#a.SupplierId],
       [b].[TypeId]      AS [#a.TypeId],
       [b].[ContactName] AS [#a.ContactName],
       [b].[Phone]       AS [#a.Phone]
FROM   [dbo].[Products] [a]
       INNER JOIN [dbo].[Suppliers] [b]
         ON ([a].[Seller.SupplierId] = [b].[SupplierId])

LeftJoin method

LINQ does not provide standard method for left join, although it is possible to describe it using other query operations. But, as you’ll see below, the sequence you must write to describe left join is pretty complicated. DataObjects.Net provides its own LeftJoin extension method for IQueryable<T> to make left join syntax more clear.

The syntax of LeftJoin is the same as syntax for standard Join, but obviously, it can be used as extension method only:

var result = session.Query.All<Product>()
  .LeftJoin(session.Query.All<Supplier>(),
    product => product.Supplier.Id,
    supplier => supplier.Id,
    (product, supplier) => new {
      product.ProductName,
      supplier.ContactName,
      supplier.Phone
    });

And the result of the LeftJoin is the similar to standard Join but join is left instead of inner:

SELECT [a].[ProductId],
       [a].[TypeId],
       [a].[ProductName],
       [b].[SupplierId]  AS [#a.SupplierId],
       [b].[TypeId]      AS [#a.TypeId],
       [b].[ContactName] AS [#a.ContactName],
       [b].[Phone]       AS [#a.Phone]
FROM   [dbo].[Products] [a]
       LEFT OUTER JOIN [dbo].[Suppliers] [b]
         ON ([a].[Seller.SupplierId] = [b].[SupplierId])

GroupJoin+SelectMany+DefaultIfEmpty pattern translation

As it was mentioned, LINQ does not provide standard LeftJoin method, but it is possible to define left join using GroupJoin, SelectMany and DefaultIfEmpty methods:

var result = from order in session.Query.All<Order>()
             join customer in session.Query.All<Customer>()
               on order.Customer equals customer into oc
             from joinedCustomer in oc.DefaultIfEmpty()
             select new {
               CustomerId = joinedCustomer.Id,
               joinedCustomer.CompanyName,
               joinedCustomer.Address.Country
             };

Translation result for this query is the same as for query with LeftJoin:

SELECT [a].[CustomerId]      AS [#c.CustomerId],
       [a].[TypeId]          AS [#c.TypeId],
       [a].[CompanyName]     AS [#c.CompanyName],
       [a].[Address.Country] AS [#c.Address.Country]
FROM   [DBO].[ORDER] [b]
       LEFT OUTER JOIN [DBO].[Customers] [a]
         ON ([b].[Customer.CustomerId] = [a].[CustomerId])

Type casts

Entity safe cast (as operator for Entity descendants) is translated with use of left join:

var result = session.Query.All<Product>()
  .Select(product => product as DiscontinuedProduct);

SELECT [a].[ProductId]           AS [#a.ProductId],
       [a].[TypeId]              AS [#a.TypeId],
       [a].[ProductName]         AS [#a.ProductName],
       [a].[Seller.SupplierId]   AS [#a.Seller.SupplierId],
       [a].[Category.CategoryId] AS [#a.Category.CategoryId],
       [a].[ProductType]         AS [#a.ProductType],
       [a].[UnitPrice]           AS [#a.UnitPrice],
       [a].[UnitsInStock]        AS [#a.UnitsInStock],
       [a].[UnitsOnOrder]        AS [#a.UnitsOnOrder],
       [a].[ReorderLevel]        AS [#a.ReorderLevel],
       [a].[QuantityPerUnit]     AS [#a.QuantityPerUnit]
FROM   [dbo].[Products] [b]
       LEFT OUTER JOIN [dbo].[Products] [a]
         ON ([b].[ProductId] = [a].[ProductId])
WHERE  ([a].[TypeId] IN (107))

References

DataObjects.NET uses left join to provide access to fields of referenced Entity:

var result = session.Query.All<Product>().Select(p => p.Category.CategoryName);

SELECT [a].[CategoryId]   AS [#a.CategoryId],
       [a].[TypeId]       AS [#a.TypeId],
       [a].[CategoryName] AS [#a.CategoryName]
FROM   [dbo].[Products] [b]
       LEFT OUTER JOIN [dbo].[Categories] [a]
         ON ([b].[Category.CategoryId] = [a].[CategoryId])

Inheritance joins

Joins are also used to gather the date from inheritance hierarchies. For example, if InheritanceSchema.ClassTable is used in a particular hierarchy, fields of any hierarchy root descendant are spanned over multiple tables there. So to gather the whole field set for a particular type, DataObjects.Net will use joins.

Let’s assume Employee is inherited form Person in hierarchy with InheritanceSchema.ClassTable. So the following query:

var employees = session.Query.All<Employee>();

will be translated with inner join of two tables:

SELECT [a].[Id],
       [a].[TypeId],
       [a].[Name],
       [b].[Salary]
FROM   (SELECT [c].[Id],
               [c].[TypeId],
               [c].[Salary]
        FROM   [dbo].[Employee] [c]) [b]
        INNER JOIN (SELECT [d].[Id],
                           [d].[TypeId],
                           [d].[Name]
                    FROM   [dbo].[Person] [d]) [a]
         ON ([a].[Id] = [b].[Id]);

Subqueries

Subqueries are either expressions of IQueryable<T> type inside projections of primary query, or aggregates calculated over such expressions.

If subquery is a part of final projection (final .Select() call), it is executed separately for each query result item during enumeration of result of original query. So in this case:

var query = session.Query.All<Person>().Select(
   employee => new {
     employee,
     Namesakes = session.Query.All<Person>()
       .Where(person => person.Name==employee.Name)
   });

// Enumerate query
foreach (var employeeData in query) {
  // Enumerate each subquery element
  foreach (Person namesake in employeeData.Namesakes) {
    // Do something with employee, namesake
  }
}

SELECT [a].[Id],
       [a].[TypeId],
       [a].[Name]
FROM   [dbo].[Person] [a];

SELECT [a].[Id],
       [a].[TypeId],
       [a].[Name]
FROM   [dbo].[Person] [a]
WHERE  ([a].[Name] = 'John');

SELECT [a].[Id],
       [a].[TypeId],
       [a].[Name]
FROM   [dbo].[Person] [a]
WHERE  ([a].[Name] = 'Susan');

Highlighted part is SQL corresponding to the main query; a set of SELECTs below are subqueries performed for each item returned by the main query.

Similar subqueries are produced for GroupBy method results: IGrouping<TKey, TElement> is also treated as a subquery.

If subquery returns scalar result, it is evaluated as part of original SQL query. In particular, this happen if subquery method chain ends with: Contains Any All Sum Average Min Max First FirstOrDefault Single SingleOrDefault

The following example illustrates subquery with All method:

var result = session.Query.All<Customer>()
  .Where(c => session.Query.All<Order>()
    .Where(o => o.Customer==c)
    .All(o => o.ShippingAddress.City==c.Address.City));

SELECT [a].[CustomerId],
       [a].[TypeId],
       [a].[CompanyName],
       [a].[ContactName],
       [a].[ContactTitle],
       [a].[Address.StreetAddress],
       [a].[Address.City],
       [a].[Address.Region],
       [a].[Address.PostalCode],
       [a].[Address.Country],
       [a].[Phone],
       [a].[Fax]
FROM   [dbo].[Customers] [a]
WHERE  (NOT EXISTS(
  SELECT *
  FROM   [dbo].[Order] [b]
  WHERE  (
     ([b].[Customer.Id] = [a].[CustomerId])
     AND
     (NOT ([b].[ShippingAddress.City] = [a].[Address.City])))
  ));

Full-text queries

DataObjects.Net allows to build full-text indexes for domain entities and execute full-text queries against database. As of DataObjects.Net 4.6, the feature is implemented for SQL Server and PostgreSQL.

Configuring full-text indexes

DataObjects.Net simplifies the task of creating full-text indexes by providing the corresponding FullText attribute, that should be applied to indexed fields. It requires one mandatory argument - the language which will be used to instruct database server to choose the appropriate resource for word breaking, stemming, and thesaurus and stopword removal as part of the query.

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

  [Field]
  [FullText("English")]
  public string Name { get; set; }
}

Querying against full-text data

Endpoint for full-text queries is Session.Query.FreeText<TEntity>(string searchCriteria)` method. But instead of returning ``IQueryable<TEntity> it returns IQueryable<FullTextMatch<TEntity>>. FullTextMatch<TEntity> is a simple class containing 2 fields: Entity and its Rank according to full-text index and search criteria.

      // Create some beverages
      new Beverage(session) {
        Name = "Sherbet Hawiian Punch"
      };
      new Beverage(session) {
        Name = "Miami-Fusion Virgin Mojito"
      };

      new Beverage(session) {
        Name = "Guava Ginger Punch"
      };

      // Query for 'punch guava' and order by rank desc
      var matches = session.Query.FreeText<Beverage>("punch guava")
        .OrderByDescending(i => i.Rank);
      foreach (var match in matches) {
        Console.WriteLine(match.Rank);
        Console.WriteLine(match.Entity.Name);
      }

      // Limit the number of matches by some rank
      var matches = session.Query.FreeText<Beverage>("punch guava")
        .Where(i => i.Rank > 0.15)
        .OrderByDescending(i => i.Rank);


Transactions

Transactional transparency

DataObjects.Net is built to simplify writing business code – a code written on high-level language (e.g. C#) and operating on application server.

This brings some new effects in comparison to the case developers faced earlier, when similar code was written in stored procedures:

  • There is an ORM transforming relational data to high-level language objects. Normally presence of ORM implies significant differences in behavior: e.g. if you remove the row in database, this happens immediately – and it’s very convenient to rely on sequence of operations there. But many ORM tools delay actual operations until explicit invocation of Session.Flush() or similar method – so basically, you can’t be sure the state you see is the same as the state in RDBMS. May be the closest analogue of this is WYSIWYG principal, and here it’s broken.
  • Frequently application server is communicating with RDBMS over network, so developers must care about reducing the chattiness between RDBMS and application server.

DataObjects.Net addresses both of these issues:

  • Transparent persistence and transactional transparency ensure you’re working with entities as if they’re “directly connected” to the underlying database objects:

    • On any reads, you get exactly the same values that must be retrieved from the database right directly. So if you just read something, direct SQL query executed on the same connection & transaction, and retrieving the same data directly, would return exactly the same value.
    • Any cached changes are persisted before any data retrievals.
    • Transaction rollbacks are immediately reflected in state. There is no necessity to update anything manually.
    • Any cached state is considered as stale on crossing the transaction boundary. For simplicity, you can imply DataObjects.Net simply invalidates it.
    • And so on. The final goal is to simulate the “direct connectivity”, but cache as much as possible without breaking this to reduce the chattiness.

    So DataObjects.Net provides “full WYSIWYG” here.

  • Generalized batching, future queries and prefetch API allow you to dramatically reduce the chattiness with almost zero efforts.

    Note

    Transparent persistence and transactional transparency allows developer to be sure that he always deal with actual state: the state he sees is absolutely the same as direct SQL queries performed on the same connection & transaction would show.

For example, if you’re fetching some entity in transaction TA and trying to get its property value in transaction TB, by default its value would be re-fetched there. “By default” indicates there are ways to make DataObjects.Net behave differently, but you must do this explicitly.

Note

As a consequence, DataObjects.Net requires any data access operations to be executed inside logical transactions. “Logical” here means that these transactions define isolation boundaries just for DataObjects.Net.

Managing transactions

To open a transaction, you should call session.OpenTransaction(...) method (it has several overloads):

using (var session = domain.OpenSession()) {
  using (var transactionScope = session.OpenTransaction()) {

    var person = new Person();
    person.Name = "Barack Obama";

    transactionScope.Complete();
  }
}

The simplest form of this method opens a transaction in active session and returns its transaction scope – an IDisposable object used to committing or rolling back the transaction.

  • To commit the transaction, call transactionScope.Complete() method and dispose the transaction scope.

  • To rollback the transaction, just dispose the transaction scope without calling Complete() method.

    Note

    In example above we open a transaction inside using block, so transaction scope returned by Open() method will be disposed on leaving this block. If code inside using block throws an exception, Complete() method won’t be invoked. So our transaction will be committed only if code inside it is executed successfully.

If session.OpenTransaction(...) method is invoked, but active session already has associated transaction, it does nothing and return so-called void scope. Such a scope does nothing on Complete() method call and its disposal as well.

You can also open a transaction with specified isolation level: session.OpenTransaction(IsolationLevel isolationLevel).

Nested transactions

Use session.OpenTransaction(TransactionOpenMode.New) to open a new transaction. If there is no outermost transaction in the active Session, the outermost transaction will be opened; otherwise a nested one will be opened.

Nesting level is limited only by underlying RDBMS.

var domain = BuildDomain();

using (var session = domain.OpenSession()) {
  using (var transactionScope = session.OpenTransaction()) {

    var john = new User {
      Name = "john doe"
    };

    // Opening a nested transaction
    using (var nestedScope = session.OpenTransaction(TransactionOpenMode.New)) {
      john.Name = "john smith";
      // Omitting nestedScope.Complete(), so nested transaction will be rolled back
    }

    Assert.AreEqual("john doe", john.Name);

    // Marking the transaction scope as completed to commit the outermost transaction
    transactionScope.Complete();
  }
}


Validation

Validation framework

Validation overview

DataObjects.Net includes consistency validation framework allowing to validate single property values, entire entities and entity graphs.

Each transaction contains its own ValidationContext – class responsible for validation of all entities changed in this transaction. Entity class implements IValidaionAware interface allowing it to be validated by validation context. When some entity is changed, it calls Validate method to inform the appropriate validation context. Validation context can be in one of following states:

  • Consistent – context immediately validates the entity by calling its OnValidate() method. Entity descendants should override OnValidate() method to validate its state (property values) and throw an exception, if state is invalid.
  • Inconsistent – context adds the entity to internal registry to validate it on entering consistent state.

If validation is failed, validation infrastructure will throw AggregateException with list of validation errors found in validated entities:

try {
  using (var transactionScope = session.OpenTransaction()) {
    using (var inconsistencyRegion = session.DisableValidation()) {

      // Change your entities here

      inconsistencyRegion.Complete();
    }
    transactionScope.Complete();
  }
}
catch(AggregateException exception) {
  Console.WriteLine("Following validation errors were found:");
  foreach (var error in exception.GetFlatExceptions())
    Console.WriteLine(error.Message);
}

You can also enforce validation of all changed entities inside inconsistency region by calling Session.Validate() method.

Validation on demand

Continuous validation can be useful in some cases, but often it is enough to validate all changed entities when transaction is being committed. In such cases you can switch validation mode to OnDemand instead of Continuous. To do this change ValidationMode property of DomainConfiguration instance before domain is built:

domainConfiguration.ValidationMode = ValidationMode.OnDemand;

When this mode is switched on, single inconsistency region is opened for each transaction, so validation context will always be in inconsistent state and entities will be validated on transaction commit only. But you still can validate changed entities with Session.Validate() method any time you want.

Object level validation rules

To define validation rules you should implement object-level validation logic in OnValidate() method that will be called on entity validation. This method should check entity state and throw an exception if it is invalid.

[HierarchyRoot]
public class Person : Entity
{
  // ...

  [Field]
  public bool IsSubscribedOnNews { get; set;}

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

  protected override void OnValidate()
  {
    base.OnValidate();

    if (IsSubscribedOnNews && string.IsNullOrEmpty(Email))
      throw new Exception("Can't subscribe on news (email is not specified).");
  }
}

Property constraints

Overview

Another way to define validation rules is to mark entity properties by special attributes – property constraints. Property constraints are special property-level validation aspects that can be applied to any property by marking it with appropriate attribute. Each constraint implements some simple validation rule for property value. For example NotNullConstraint ensures that property value is not null, RegexConstraint ensures that string value matches specified regular expression pattern.

[LengthConstraint(Min = 2, Max = 128)]
[NotNullOrEmptyConstraint]
public string FirstName { get; set;}

[PastConstraint]
public DateTime BirthDay { get; set; }

[EmailConstraint]
public string Email { get; set;}

Each property constraint attribute has two additional properties: Message and Mode.

Message property specifies message for exception that should be thrown if property value is invalid. Message can contain some special variables, that will be replaced automatically: Property value – {value}, property name – {PropertyName} and constraint parameters – {[parameter name]}.

[PastConstraint(Message = "Birth day must be in the past.")]
public DateTime BirthDay { get; set; }

[RangeConstraint(
  Min = 0.8,
  Max = 2.13,
  Message = "Incorrect '{PropertyName}' value: {value}, " +
    "it can not be less than {Min} and greater than {Max}.")]
public double Height { get; set;}

If we set height property to 2.5, we’ll get exception with such message: “Incorrect ‘Height’ value: 2.5, it can not be less than 0.8 and greater than 2.13.”

By default property constraints are used when entity is being validated, but it’s also possible to throw exception on setting new value to the property. PropertyConstraintAspect has special property Mode of ConstrainMode type. It has two available values: OnValidate (default value) and OnSetValue.

[PastConstraint(Mode = ConstrainMode.OnSetValue)]
public DateTime BirthDay { get; set; }

Pre-defined validation aspects

DataObjects.Net validation framework includes following predefined constraints:

  • EmailConstraint – Ensures that email address is in correct format
  • FutureConstraint – Ensures that date value is in the future
  • LengthConstraint(Min, Max) – Ensures field length (or item count) fits in specified range
  • NotEmptyConstraint – Ensures that property value is not empty string
  • NotNullConstraint – Ensures property value is not null
  • NotNullOrEmptyConstraint – Ensures that property value is not null or empty string
  • PastConstraint – Ensures that date value is in the past
  • RangeConstraint(Min, Max) – Ensures field value fits in the specified range
  • RegexConstraint(Pattern) – Ensures property value matches specified regular expression

Implementing custom constraints

It is possible to implement your own property constraints inheriting it from abstract class PropertyConstraintAspect.

[Serializable]
public class PhoneNumberConstraint : PropertyConstraintAspect
{
  private const string PhoneNumberPattern = "^[2-9]\\d{2}-\\d{3}-\\d{4}$";

  [NonSerialized]
  private Regex phoneNumberRegex;

  public override bool IsSupported(Type valueType)
  {
    return valueType==typeof(string);
  }

  public override bool CheckValue(object value)
  {
    string phoneNumber = (string) value;
    return
      string.IsNullOrEmpty(phoneNumber) ||
      phoneNumberRegex.IsMatch(phoneNumber);
  }

  protected override string GetDefaultMessage()
  {
    return "Phone number is incorrect";
  }

  protected override void Initialize()
  {
    base.Initialize();
    phoneNumberRegex = new Regex(PhoneNumberPattern, RegexOptions.Compiled);
  }
}

We create regular expression instance in Initialize() method instead of constructor, we do this since constraints is PostSharp aspects and it is created in compile time also, but we need regex to be created in runtime only.

[PhoneNumberConstraint]
public string Phone { get; set;}


Getting ideal performance

Statement batching

Batching is an ability of DataObjects.NET to join sequences of individual SQL statements into batches, so each batch is sent as part of a single roundtrip to database server.

DataObjects.Net batch can be formed of the following “parts”:

  • Individual CRUD instructions (CReate, Update, Delete statements)
  • Future queries (prefetch API utilizes them as well)
  • Up to one regular query or fetch command (just because its result is necessary “right now”, i.e. there it is always either just one or absents in batch queue)

All these cases were shown in scenario described below (future queries example), so no separate batching example is necessary here.

Lazy loading

For loading data from a database into memory it’s handy to design things so that as you load an object of interest you also load the objects that are related to it. This makes loading easier on the developer using the object, who otherwise has to load all the objects he needs explicitly. However, if you take this to its logical conclusion, you reach the point where loading one object can have the effect of loading a huge number of related objects – something that hurts performance when only a few of the objects are actually needed. Lazy loading is, in fact, an agreement on interrupting this greedy loading process on certain relationships or properties. But to ensure the further availability of not yet loaded parts of the graph, availability markers inside each fetched object are maintained so that if not yet loaded data is requested, it would be loaded.

Default lazy loading behavior

DataObjects.Net supports lazy loading for all persistent properties except primary keys:

  • References to entity – persistent fields of type IEntity (and thus Entity) and its implementers;

  • EntitySets – persistent fields of type EntitySet<T> and its inheritors;

  • Structures – persistent fields where field type is inheritor of Structure;

  • Regular fields – persistent fields of primitive type such as int, string, DateTime, nullable types, arrays of primitive types, etc.

    Note

    References to entity and EntitySets are lazy load fields by default.

See the following example:

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

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

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

  [Field(LazyLoad = true, Length = 65536)]
  public byte[] Photo { get; set; }

  [Field(LazyLoad = true, Length = 8192)]
  public byte[] Avatar { get; set; }

  [Field(LazyLoad = true)]
  public Address Address { get; set; }

  [Field]
  public Person Manager { get; private set; }

  [Field]
  [Association(PairTo = "Manager")]
  public EntitySet<Person> Employees { get; private set; }
}

public class Address : Structure
{
  [Field(Length = 60)]
  public string Street { get; set; }

  [Field(Length = 15)]
  public string City { get; set; }

  [Field(Length = 15)]
  public string Region { get; set; }

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

  [Field(Length = 15)]
  public string Country { get; set; }
}

In this example the following fields are subject of lazy loading:

  • Manager property is entity reference. It isn’t explicitly marked as lazy, so key of referenced Manager will be loaded by default, but Manager instance it identifies will be resolved only on attempt to read the value of this property.
  • Employees property is EntitySet. All EntitySets are lazy – nothing additional is loaded for each EntitySet by default. Even EntitySet instance is created on the first attempt to access it. More complete description of EntitySet loading behavior is provided below.
  • Address property is structure. Structures are simply aggregates containing sets of fields, they aren’t lazy by default, but in this case the whole aggregate is lazy. So none of its fields will be loaded by default.
  • Photo and Avatar are regular persistent fields. They’re marked as lazy here, so their values will be fetched on the first attempt to read them. By default they won’t A query returning Person objects won’t pull values of these properties by default.

Reference fields

Reference fields are internally represented as a set of fields (columns) forming the key of referenced object – so there is no reference to materialized Entity or its Key. Consequences:

  • We are able to materialize it at any moment: we must just create Key object and resolve it using Session.Query.SingleOrDefault<T>(Key key) method. Actually this is very close to actual process.
  • All the materialized objects aren’t tightly bound with each other, so garbage collector is able to reclaim the memory used by any of them, that isn’t directly referenced from your code. Of course, this isn’t always true – e.g. to make this work, default Session cache (SessionCacheType.LruWeak) must be used. This solution allows you to process unlimited sets of objects sequentially even in a single transaciton.

So marking a reference field as [Field(LazyLoad=true)] will lead to lazy loading of reference key (foreign key value); reference itself is anyway resolved lazily.

Thus marking a reference field as [Field(LazyLoad=true)] is not recommended. Reading additional value per each row is normally quite cheap (keys are usually short), but getting additional rountrip to database for it is not.

Collection properties (EntitySets)

EntitySet is intrinsically lazy:

  • If property of EntitySet type isn’t read accessed yet, EntitySet isn’t even created. So EntitySet creation is lazy.

  • EntitySet maintains its own state. EntitySet state answers on the following questions:

    • Is EntitySet content loaded or not, fully or partially?
    • What’s the count of items in EntitySet, is it known at all or not?
    • What are known keys of contained entities? Does this set contain some specific key?

  • State of any created EntitySet is set to “nothing is loaded yet”.

  • If EntitySet state is “nothing is loaded yet”, any attempt to access its properties or methods, that must lead to partial state loading, leads to a request, that can be described as “try to load the state completely first”. Prcesily, EntitySet asks prefetcher to load up to 32 of items it must contain:

    • if result contains less than 32 items, it’s clear that EntitySet state is now “fully loaded”, so any subsequent operations won’t lead to any fetches.
    • Otherwise, its state is set to “partially loaded”, and fetched items will be added to its “known items” set. If loaded part of its state already contains the answer to requested operation (e.g. if user invoked .Contains method, and key it was looking for is located in “known items” set now), nothing additional will happen. Otherwise an additional request will be performed (e.g. loading of actual Count property value).

  • EntitySet enumeration leads to full state loading. So if enumeration is the first operation on a particular EntitySet in the current transaction, there will be no partial state loading attempt.

  • If a particular read operation on EntitySet has completed, its subsequent invocation in the same transaction won’t lead to database hit.

    Note

    Such a “greedy” partial state loading behavior is logical: DataObjects.Net is able to perform about 10K queries per second, buts materialization speed reaches 400K entities per second. This means there is almost no difference between fetching 1 entity or 40. That’s why DO4 tries to load more even you didn’t ask it for this: it does this because even if there are all 32 items, the cost of loading all of them will be similar to loading one. High probability of getting less than 32 items is another reason for doing this: we assume most of collections are either empty, or contain just few items, and only some of them are filled by hundreeds of items. So we get all the contents of most collections by a single query instead of torturing RDBMS by multiple ones. Note that partial state loading may expose negative effect on large collections: if some collection contains e.g. 1000 items, an attempt to read its Count property will first lead to partial state loading query, and then to a query fetching Count value. On the other hand, you can deal with this case using our prefetch API. Later we’ll offer more options allowing to control partial state loading behavior more precisely.

Marking a EntitySet field as [Field(LazyLoad=true)] is impossible: such fields are intrinsically lazy.

Regular or Structure properties

[Field(LazyLoad=true)] ensures undrelying columns behing such a property won’t be requested from database:

  • On fetches (Session.Query.Single<T>(...) method group)
  • On queries, if they aren’t explicitly selected in final projection. .Select(person => new {Person = person, Photo = person.Photo}) is an example of such selection.

If field marked by [Field(LazyLoad=true)] is not fetched, its value will be loaded on attempt to read it.

But it’s important to know that generally any field in DataObjects.Net can be actually loaded lazily, even if it isn’t marked as lazy loading field. This is related to inheritance:

  • If you query for Shape objects, DataObjects.Net requests only the fields declared in this type and its ancestors. So fields declared in Circle object (e.g. Radius) will not be loaded by default, even if particular fetched instance is Circle.
  • This is related to fetches as well. Methods from Session.Query.Single<T>(...) method group either fetch all the fields of exact type, if it’s known (by default DataObjects.Net keeps cached up to 16K types of entities it recently dealed with – it considers type of entity never changes during its lifetime). If it’s unknown, it fetches just fields of specified type T; the query it sends delivers a type discriminator as well, so actual entity type will also be cached.
  • Any attempt to load some field value will lead to a query fetching all non-lazy values that must be loaded in additional to requested value. So if you fetched Rectangle as Shape, attempt to read its Width property will lead to loading of both Width and Height values, if both were not available.
  • Prefetch API allows to load the complete state of sets of partially loaded entities much faster.

Let’s look at prefetch API usage example for persitent model we defined earlier:

var persons = session.Query.All<Person>()
  .Prefetch(p => p.Avatar)
  .Prefetch(p => p.Photo)
  .Prefetch(p => p.Employees);
foreach (var person in persons) {
  // Some code here...
}

As you see, here we make sure all the lazy properties including EntitySet are loaded before further processing using the way minimizing database chattiness (prefetch API).

Prefetch API

Prefetch API provides a way of grouping Entity or EntitySet state fetch request together to execute multiple of them during a single query or database server roundtrip. DataObjects.Net combines individual prefetch queries together and joining the resulting ones in batches (so it’s a part of generalized batching feature).

Prefetch to IQueryable<T>

Let’s see prefetch API in action:

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

  [Field(LazyLoad = true, Length = 65536)]
  public byte[] Photo { get; set; }

  [Field]
  public Person Manager { get; set; }

  [Field]
  [Association(PairTo = "Manager")]
  public EntitySet<Person> Employees { get; private set; }
  ...
}

var persons = session.Query.All<Person>()
  .Prefetch(p => p.Photo) // Lazy load field
  .Prefetch(p => p.Employees // EntitySet Employees
    .Prefetch(e => e.Photo)) // and lazy load field for each EntitySet item
  .Prefetch(p => p.Manager); // Referenced entity
foreach (var person in persons) {
  // some code here...
}

This tiny piece of code ensures the following data will be available without any additional roundtrips to a database:

  • Person.* - all primitive non-lazy fields
  • Person.Manager - reference field
  • Person.Photo - lazy loading field
  • Person.Employees - EntitySet field
  • Person.Photo - lazy loading field for any item in Person.Employees

As you see, we use the same .Prefetch(...) method everywhere, no matter whether we should handle primitive field, reference field, EntitySet or make a nested prefetch call.

Take a look at the resulting SQL queries that are executed by DataObjects.Net for this prefetch expression tree:

-- Batch 1
SELECT [a].[Id], 101 AS [TypeId], [a].[Name], [a].[BirthDay], [a].[Manager.Id]
FROM [dbo].[Person] [a];

-- Batch 2
SELECT [a].[Id], [a].[TypeId], [a].[Photo] FROM (
  SELECT [b].[Id], 101 AS [TypeId], [b].[Name], [b].[BirthDay],
    [b].[Photo], [b].[Manager.Id]
  FROM [dbo].[Person] [b]) [a]
WHERE [a].[Id] IN (@p1_0_0_0, @p1_0_1_0);

SELECT [a].[Id], 101 AS [TypeId], [a].[Name], [a].[BirthDay], [a].[Manager.Id]
FROM [dbo].[Person] [a]
WHERE ([a].[Manager.Id] = @p2_0);

SELECT [a].[Id], 101 AS [TypeId], [a].[Name], [a].[BirthDay], [a].[Manager.Id]
FROM [dbo].[Person] [a]
WHERE ([a].[Manager.Id] = @p3_0);

Notice, after the execution of the first query DataObjects.Net performs an additional batch to fetch the rest of the data. Actually there could be a set of such batches – this depends on the complexity of prefetch expression tree.

Prefetch to Enumerable<T>

Prefetch API in DataObjects.Net actually isn’t bound to IQueryable<T> provider – it is a completely separate API based on low-level delayed queries implementation. DataObjects.Net itself relies on it to load necessary parts of state of Entities. As a result, prefetch API can be used for any IEnumerable<T> – i.e. the original collection must not be necessarily an IQueryable<T>. The following code leads to absolutely the same queries as the one above:

var personIds = session.Query.All<Person>().Select(p => p.Id);
var prefetchedPersons = session.Query.Many<Person, int>(personIds)
  .Prefetch(p => p.Photo)
  .Prefetch(p => p.Employees
    .Prefetch(e => e.Photo))
  .Prefetch(p => p.Manager);
foreach (var person in prefetchedPersons) {
  // some code here...
}

Syntactic sugar (shortcuts) in Prefetch API

Starting from DataObjects.Net 4.4, Prefetch API provides a nice shortcut for describing several Prefetch expressions on the same node as anonymous type expression. This simplifies the overall Prefetch expression tree and make it more clean and readable:

var persons = session.Query.All<Person>())
    .Prefetch(p => new { p.Photo, p.Manager })
    .Prefetch(p => p.Employees
    .Prefetch(e => new { e.Photo, e.Manager }));

Note the usage of the construct: Prefetch(p => new { p.Photo, p.Manager }), it does exactly the same as the following code but with less efforts

.Prefetch(p => p.Photo)
  .Prefetch(p => p.Manager)

Delayed (future) queries

DataObjects.Net allows to execute a set of queries as a single batch (roundtrip to database server).

Again, let’s show this on example:

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

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

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

  [Field]
  public Person Manager { get; set; }

  [Field]
  [Association(PairTo = "Manager")]
  public EntitySet<Person> Employees { get; private set; }

  public override string ToString()
  {
    return Name;
  }
}

var employee = new Person {Name = "Employee"};
var manager  = new Person {Name = "Manager"};
manager.Employees.Add(employee);

var simpleCompiledQuery = session.Query.Execute(() =>
  from person in session.Query.All<Person>()
  orderby person.Name
  select person
  );
var managedPersonCount = session.Query.ExecuteFutureScalar(() => (
  from person in session.Query.All<Person>()
  where person.Manager!=null
  select person
  ).Count());
var personsWithEmployees = session.Query.ExecuteFuture(() =>
  from person in session.Query.All<Person>()
  where person.Employees.Count!=0
  select person
  );

Console.WriteLine("All persons: {0}",
  simpleCompiledQuery.ToCommaDelimitedString());
Console.WriteLine("Managed person count: {0}",
  managedPersonCount.Value);
Console.WriteLine("Person with employees: {0}",
  personsWithEmployees.ToCommaDelimitedString());

As you noticed, we use Session.Query.ExecuteFutureXxx methods here to delay query execution.

Notice that Session.Query.Execute call was also not executed “immediately”: any compiled query is executed on its enumeration. So in this case its enumeration lead to execution of the whole bunch of queued future queries.

Note

How many batches DataObjects.Net sends to database server to perform the whole code shown above?

Correct answer is just one batch:

-- Batch 1
exec sp_executesql N'
INSERT INTO [dbo].[Person] ([Id], [Name], [BirthDay], [Manager.Id])
VALUES (@p1_0, @p1_1, @p1_2, @p1_3);

INSERT INTO [dbo].[Person] ([Id], [Name], [BirthDay], [Manager.Id])
VALUES (@p2_0, @p2_1, @p2_2, @p2_3);

SELECT COUNT_BIG(*) AS [column]
FROM (
  SELECT [a].[Id], 101 AS [TypeId], [a].[Name], [a].[BirthDay], [a].[Manager.Id]
  FROM [dbo].[Person] [a]
  WHERE ([a].[Manager.Id] IS NOT NULL)) [b];

SELECT [a].[Id], 101 AS [TypeId], [a].[Name], [a].[BirthDay], [a].[Manager.Id]
FROM [dbo].[Person] [a]
WHERE ((SELECT COUNT_BIG(*) FROM (
  SELECT [b].[Id], 101 AS [TypeId], [b].[Name], [b].[BirthDay], [b].[Manager.Id]
  FROM [dbo].[Person] [b]
  WHERE ([b].[Manager.Id] = [a].[Id])) [c]) <> 0);

SELECT [a].[Id], [a].[TypeId], [a].[Name], [a].[BirthDay], [a].[Manager.Id]
FROM (
  SELECT [b].[Id], 101 AS [TypeId], [b].[Name], [b].[BirthDay], [b].[Manager.Id]
  FROM [dbo].[Person] [b]) [a]
ORDER BY [a].[Name] ASC;

So future queries in conjunction with generalized statement batching is a very powerful feature.


Database schema upgrade

Database schema upgrade overview

Initial database schema creation

As you already know DataObjects.Net automatically generates database schema when you build a domain. You don’t need to create tables and columns manually, everything you need to do is just to create empty database and specify appropriate connection string in domain configuration.

Let us suppose we have created our model consisting of one class:

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

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

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

Then build domain:

var domainConfig = new DomainConfiguration("sqlserver://localhost/myDatabase");
domainConfig.Types.Register(myAssembly, "MyProduct.Model");
domainConfig.UpgradeMode = DomainUpgradeMode.Recreate;
var domain = Domain.Build(domainConfig);

When domain is built, database structure will look like this:

You can see that there is Order table corresponding to Order entity in our model and several additional tables.

Existing database schema upgrade

You can also notice that in previous example we specified DomainUpgradeMode.Recreate as upgrade mode in domain configuration, it means that existing database schema and data should be completely cleared before new schema is generated. This mode is very useful when you want to create new database or perform unit tests, but when our application is in use we need to upgrade database schema from one version to another without loosing existing data, moreover sometimes existing data should be also modified, e.g. we may want to change some column type or move some data from one class to another.

There are several available values of DomainUpgradeMode :

  • Validate – Domain will check whether database schema is compatible with persistent model, anyway it will not be modified. If schema is incompatible, exception will be thrown.
  • Recreate – Database schema will be completely recreated, all existing data will be removed.
  • Perform – Storage upgrade will be performed. All missing database structures (columns, tables) will be added, excess ones will be removed.
  • PerformSafely (default value) – The same as Perform mode, but any actions leading to data or precision lost are never performed to the database unless all of them are explicitly enabled.

Automatic upgrade

In order to upgrade exiting database schema and save stored data we can use Perform or PerformSafely mode. In simple cases such upgrade can be performed fully automatically.

For example let’s create several orders and then change our model: Add Customer class and Customer property to Order class:

[HierarchyRoot]
public class Order : Entity
{
  // ...

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

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

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

If we build our domain with Validate mode, we’ll get SchemaSynchronizationException in Build() method. If we build it with Recreate mode, it will create new database schema, but data will be lost. When we have our domain built with Perform or PerformSafely upgrade mode, we’ll see following database structure:

DataObjects.Net automatically added Customer table and Customer.Id column to Order table. New table Customer is empty and value of new field is null, but all existing orders are saved.

Upgrade handlers and hints

Automatic upgrade works perfectly when we add new classes and properties to our model, but sometimes we need to perform some specific schema modifications, for example we can rename some properties or classes.

Rename hints

Let’s rename Customer class to Person and, for example, Name property to FullName.

[HierarchyRoot]
public class Order : Entity
{
  // ...

  [Field]
  public Person Customer { get; set;}
}

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

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

To upgrade schema correctly, we should inform domain that Person class is renamed Customer, otherwise it will remove Customer table with all records and create new empty Person table. To do this we use so called upgrade handlers and upgrade hints.

Upgrade handler is class responsible for upgrade of persistent classes from one assembly. It must be inherited from UpgradeHandler or implement IUpgradeHandler interface, it also must have a parameterless constructor. Each assembly with persistent model should contain exactly one upgrade handler class, it will be automatically found when schema is being upgraded. UpgradeHandler class contains a set of methods and properties that can be overridden for customizing schema upgrade process.

public class Upgrader : UpgradeHandler
{
  protected override void AddUpgradeHints(Xtensive.Collections.ISet<UpgradeHint> hints)
  {
    hints.Add(new RenameTypeHint("MyProduct.Model.Customer", typeof (Person)));
    hints.Add(new RenameFieldHint(typeof (Person), "Name", "FullName"));
  }
}

In our example we have overriden AddUpgradeHints method to add two hints. If we build domain with new model it will find our upgrade handler and will rename columns and tables instead of removing and creating new ones, so their data will be saved.

  • RenameFieldHint describes a renaming of a persistent field. You should create this hint when you rename a persistent field.
  • RenameTypeHint describes a renaming of a persistent type. You should create this hint when you rename entities or structures.

Remove hints

In order to understand meaning of remove hints let’s remember difference between Perform and PerformSafely upgrade modes. Second one requires all modifications leading to losing data to be explicitly permitted. For example, if we remove some persistent field or persistent class from our model, domain builder will remove appropriate database structure in Perform mode and will throw exception in PerformSafely mode. To perform such operations in safe mode we should use following hints:

  • RemoveFieldHint describes a renaming of a persistent field.
  • RemoveTypeHint describes a renaming of a persistent type.

For example, if you want to remove Quantity field from Order in PerformSafely mode, you would remove appropriate property from Order class and add RemoveFieldHint to hints collection in AddUpgradeHints method.

Change type hint

ChangeFieldTypeHint is used when you want to change type of some persistent column. For example you have Order class with Number property of int type, but new version of application should allow to store numbers with literal prefix, and we definitely want to change column type to string and convert all stored numbers. To do this we change type of property to string:

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

And add ChangeFieldTypeHint in our custom upgrade handler:

hintSet.Add(new ChangeFieldTypeHint(typeof(Order), "Number"));

If hint is not added domain builder will create new column without saving data. In safe mode an exception will be thrown.

You should also use ChangeFieldTypeHint hint if you use PerformSafely mode change length of string field or precision of decimal field.

When column type is changed domain builder uses standard functions of RDBMS to convert values. In our example we changed int type to string and values were successfully converted. But if types are incompatible in particular RDBMS, we’ll get appropriate exception.

Move and copy field hints

Imagine we have Order type inherited from Document, Order class has Number property. We want number to be a property of Document class. We can declare such property in Document and remove it from Order, but we also have to copy data from Order table to Document table before order’s column will be removed.

To do this we can use MoveFieldHint, that moves some field data to another class within single hierarchy:

hintSet.Add(new MoveFieldHint("MyProduct.Model.Order", "Number", typeof(Document)));

Whereas MoveFieldHint implies that original field is removed, CopyFieldHint don’t, it just copies. To implement our upgrade handler using CopyFieldHint we should add RemoveFieldHint.

hintSet.Add(new CopyFieldHint("Order", "Number", typeof(Order)));
hintSet.Add(new RemoveFieldHint("Order", "Number"));

Recycled types and fields

There are some cases where automatic upgrade with upgrade hints is not enough to describe changes in our model. For example suppose we decided to extract product information from Order class to separate Product class. In original model we have Order.ProductName property of string type. In result model we have Order.Product property of Product type, which has Name property.

So we should enumerate all orders during upgrade process, create new product for each unique product name, and assign newly created products to Product property of each order. We can not do this when domain already built because column ProductName should be removed from Order table as a result of schema upgrade. Since it is rather complex operation, we will perform it manually.

First of all we should make necessary changes to our model – add new persistent class and persistent field. Then we mark old field ProductName as recycled, it is also recommended to mark it as obsolete:

[HierarchyRoot]
public class Order : Entity
{
  // ...

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

  [Field, Obsolete, Recycled]
  public string ProductName { get; set; }
}

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

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

Next thing we should do is to implement our custom upgrade handler for this upgrade:

public class Upgrader : UpgradeHandler
{
  public override void OnUpgrade()
  {
    var session = Session.Demand();
    foreach (var order in session.Query.All<Order>()) {
      var product = session.Query.All<Product>()
         .SingleOrDefault(p => p.Name==order.ProductName);
      if (product==null)
        product = new Product { Name = order.ProductName };
      order.Product = product;
    }
  }
}

As you can see, we have overridden OnUpgrade() method of upgrade handler and placed our custom logic there. We manually query the orders and create new products. When this method is executed, both old and new fields are accessible. But when domain is already built, you can’t use recycled fields; moreover, their underlying tables and columns are removed at that moment.

In our example we used recycled fields, but it is also possible to use recycled persistent classes in exactly the same way. You need just mark types to be removed by [Recycled] attribute and recycle their data in OnUpgrade() method.

Legacy database schema support

DataObjects.Net can be used for mapping of entities to existing database. There are two special upgrade modes for this:

  • DomainUpgradeMode.LegacySkip skips any schema checks.
  • DomainUpgradeMode.LegacyValidate ensures all mapped structures (tables and columns) are available in database; fails with exception if something is absent.

Currently legacy mode has a set of limitations:

  • IDENTITY columns are not yet supported - you have to use generators or write your own ones.
  • Mapping to multiple schemas is not supported.
  • Mapping to views and stored procedures isn’t supported.
  • T4-based model code generation (ActiveRecord pattern) is not available, i.e. you should write the classes by your own.
  • If you’re going to use non-paired EntitySets, you can’t define table and column mappings for runtime-generated type that is used to represent its items. Usage of such non-paired EntitySets is optional, but this is pretty convenient to use them in many cases.
  • Also, you can’t fully control mappings for composite foreign keys.

Rules to follow:

  • Use DomainUpgradeMode.LegacySkip or DomainUpgradeMode.LegacyValidate to to turn this feature on.
  • If mapped table and column names differ from derived by default mapping rules, use [TableMapping] and [FieldMapping] attributes.
  • You must explicitly specify type discriminators (using [TypeDiscriminator] and [TypeDiscriminatorValue(...)] attributes), if you’re going to map hierarchies with inheritance. Note that inheritance hierarchies can also be represented as non-inherited types containing reference as key of “descendant” type.
  • Take all the above-mentioned limitations into account.

Here is an example of Legacy model mapping. Note that we are mapping 3 classes to the single table “Animals” and use ElementType property as type discriminator.

[HierarchyRoot(InheritanceSchema = InheritanceSchema.SingleTable)]
[TypeDiscriminatorValue("Animal", Default = true)]
[TableMapping("Animals")]
public class Animal : Entity
{
  [Field, Key]
  public Guid Id { get; private set; }

  [Field(Length = 50), TypeDiscriminator]
  [FieldMapping("Type")]
  public string ElementType { get; private set; }

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

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

  [Field]
  [FieldMapping("Owner")]
  public Person Owner { get; set; }
}

[TypeDiscriminatorValue("Dog")]
public class Dog : Animal
{
}

[TypeDiscriminatorValue("Cat")]
public class Cat : Animal
{
}


Advanced topics

Unified exceptions

DataObjects.Net translates ADO.NET provider-specific exceptions to its own provider-independent exceptions. This should dramatically simplify any code relying on clasas on underlying exception, such as transactional reprocessing code.

Base class for any storage-level exception is named StorageException: It also serves as a base class for exceptions that are not related to SQL execution errors.

This class has many decendants. The most important are OperationTimeoutException, SyntaxErrorException, ReprocessableException, ConstraintViolationException.

Let’s look at them. The first is OperationTimeoutException. It’s thrown when current operation timed-out. This might happen if server became unavailable or internal timeout reached.

SyntaxErrorException is thrown when server didn’t accept the generated SQL query. Typically such error means that either RDBMS does not support particular feature or DataObjects.Net generated an invalid SQL.

ReprocessableException is a base class for exceptions related to transaction isolation errors. It has two decendants: DeadlockException and TransactionSerializationFailureException. First is thrown when dead-lock occured during execution and current transaction has been chosen as a victim to resolve dead-lcok. The second exception is thrown for other transaction isolation-related errors.

Finally, there is ConstraintViolationException. As the name suggests it wraps errors related to RDBMS constraints on data. It has three descendants for each constraint type: CheckConstraintViolationException, ReferentialConstraintViolationException, UniqueConstraintViolationException.

CheckConstraintViolationException is thrown when CHECK constraint is violated. It also thrown in the case of violation of NOT NULL constraint.

ReferentialConstraintViolationException is used for referential constraint errors. In other words, it’s thrown when FOREIGN KEY is not satisfied. This could mean inserting row that references non-existent row or removing row that is being referenced by other rows. DataObjects.Net handles references for you, so generally you should not get this exception.

UniqueConstraintViolationException wraps errors for PRIMARY KEY and UNIQUE constraints. Also it’s used for duplication errors with unique indexes.

Versions and concurrency control

DataObjects.Net supports both optimistic and pessimistic concurrency control:

  • Entity.Lock method and IQueryable<T>.Lock extension methods provide support for pessimistic concurrency control;
  • VersionInfo type and a set of supplementary classes (VersionSet, VersionValidator, VersionCapturer) provide support for optimistic concurrency control.

Pessimistic concurrency control: Lock method group

Lock methods put shared or exclusive lock on the specified rows in the database. Entity.Lock method puts lock on rows that correspond to Entity instance. Let’s take a look at quick example:

// Increments number of friends for the specified user
public void IncrementFriendsCountWithLock(User user)
{
    // Exclusively locks rows corresponding to the specified `User` entity.
    // If this object is already locked by other transaction an exception
    // would be thrown.
    user.Lock(LockMode.Exclusive, LockBehavior.ThrowIfLocked);

    // After lock has been obtained change to object
    user.FriendsCount++;
}

IQueryable<T>.Lock extension method puts lock on rows that correspond to the underlying query result. Here is example of its usage:

// Set IsArchied flag on all documents that were created earlier than
// specified date.
public void ArchiveOldDocuments(DateTime boundary)
{
    // Query the data and put exclusive lock on all obtained rows.
    var documents = session.Query.All<Document>
        .Where(doc => doc.Date < boundary)
        .Lock(LockMode.Exclusive, LockBehavior.ThrowIfLocked)
        .ToList();

    // Modify the document.
    foreach (var doc in documents)
        doc.IsArchived = true;
}

Lock() methods accept two arguments LockMode and LockBehavior. These control type of the lock to obtain and behavior in the case lock could not be obtained.

Locking is rather complex mechanism deeply integrated into almost any RDBMS to provide transaction isolation. If, after reading this part, you’ve got an imagination this is something simple, most likely you’re wrong. Transaction isloation-related concepts are frequently musinderstood. So if you feel you don’t fully udnerstand this (e.g. when locks are placed automatically, what types of locks are there, what is index range lock, what happens when you try to lock a resource that is already locked, what difference between locking and MVCC, etc.), we recommend you to read more about this. You can start e.g. from this article, and go further until you’ll be fluent with all the terms (you should already know the keywords).

Optimistic concurrency control: VersionInfo and related types

DataObjects.Net deals with versions differently in comparison to all the other ORM tools we know – its version control API is really flexible and powerful.

VersionInfo type is the most important part of it:

  • It is a serializable struct actually containing Tuple inside it. It is designed to abstract the actual version implementation for developers – i.e. we use the same approach as we did with Key type.
  • VersionInfo objects can be compared for equality;
  • Entity.VersionInfo returns current version of entity. If you’ll compare this version with the one you previously stored somewhere, you can determine whether the entity was changed or not.
  • VersionInfo describes both automatically provided and manually configured versions – DataObjects.Net takes care of building it properly from underlying entity fields.
  • There are useful ToString, Format and Parse methods, so any VersionInfo can be serialized to string with ease. This can be really useful in web applications (e.g. you can serialize a version of edited object to a hidden field).

There are entities with manual and automatically provided versions:

  • If you don’t use [Version] attribute at hierarchy root type, DataObjects.Net considers it must maintain a version for any type from this hierarchy automatically. In this case value of any non-lazy field becomes a part of version.
  • Otherwise only field(s) marked by [Version] form Entity‘s version:
    • By default DataObjects.Net tries to increment (or assign an approproate version value to) each of such fields on object modification (but just once per transaction). So if there is just one field marked by version, everything must work autimatically. Default implementation relies on VersionGenerator type to build next version values, all the implementation details are fully described here.
    • But you can override this by overriding Entity.HandleUpdateVersionInfo(...) method. Don’t call base method from new one to disable default logic.

Finally, there are few supplementary types simplifying version validation:

  • VersionSet is dictionary-like serializable collection storing the information about object versions
  • VersionCapturer fills VersionSet by capturing version information (Key-VersionInfo pairs) associated with either accessed or modified objects in particular Session. Use VersionCapturer.Attach method to attach it to a Session.
  • VersionValidator, on contrary, validates versions of changing objects in Session relying on either provided VersionSet or delegate returning expected VersionInfo for a particular entity by its Key. It doesn’t simply compare VersionInfo: DataObjects.Net already has some elements allowing it to deal with future appearance of caching APIs, in particular, it is aware if state of particular Entity, that is known to Session, is stale (i.e. it is taken from cache) or not. So if entity state isn’t stale, VersionValidator compares its VersionInfo; otherwise, it fetches entity version directly from the database to ensure that actual version was validated.

Below is sample code utilizing all these features.

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

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

  [Field(Length = 200)]
  public string SecondName { get; set; }

  [Field]
  [Association(PairTo = "Friends")]
  public EntitySet<Person> Friends { get; private set; }

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

  public string FullName {
    get {
      return "{Name} {SecondName}".FormatWith(this);
    }
  }

  public override string ToString()
  {
    return ToString(false);
  }

  public string ToString(bool withFriends)
  {
    if (withFriends)
      return "Person('{0}', Friends={{1}})"
        .FormatWith(FullName, Friends.ToCommaDelimitedString());
    else
      return "Person('{0}')".FormatWith(FullName);
  }

  public Person(string fullName)
  {
    var pair = fullName.RevertibleSplitFirstAndTail('\\', ',');
    SecondName = pair.First.Trim();
    Name = pair.Second.Trim();
  }
}

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

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

  [Field, Version]
  // If HandleVersionInfoUpdate() isn't implemented, default implementation
  // (relying on VersionGenerator.Next(...) method) is used.
  public int Version { get; private set; }

  [Field]
  [Association(PairTo = "Company")]
  // By default any EntitySet change also leads to Version change
  public EntitySet<Person> Employees { get; private set; }

  public override string ToString()
  {
    return ToString(true);
  }

  public string ToString(bool withPersons)
  {
    if (withPersons)
      return "Company('{0}', Employees: {1})"
        .FormatWith(Name, Employees.ToCommaDelimitedString());
    else
      return "Company('{0}')".FormatWith(Name);
  }

  public Company(string name)
  {
    Name = name;
  }
}

[Test]
public void CombinedTest()
{
  var domain = GetDomain();

  using (var session = domain.OpenSession()) {
    // Auto transactions!
    var alex = new Person("Yakunin, Alex");
    var alexVersion = alex.VersionInfo;
    Dump(alex);
    var dmitri = new Person("Maximov, Dmitri");
    var dmitriVersion = dmitri.VersionInfo;
    Dump(dmitri);

    alex.Friends.Add(dmitri);
    // Automatically provided versions
    // won't change because of change in EntitySet.
    Assert.AreEqual(alexVersion, alex.VersionInfo);
    Assert.AreEqual(dmitriVersion, dmitri.VersionInfo);

    var xtensive = new Company("X-tensive.com");
    var xtensiveVersion = xtensive.VersionInfo;
    Dump(xtensive);

    string newName = "Xtensive";
    Console.WriteLine("Changing {0} name to {1}", xtensive.Name, newName);
    xtensive.Name = newName;
    Dump(xtensive);
    Assert.AreNotEqual(xtensiveVersion, xtensive.VersionInfo);
    xtensiveVersion = xtensive.VersionInfo;

    Console.WriteLine("Xtensive.Employees.Add(Alex)");
    xtensive.Employees.Add(alex);
    Dump(xtensive);
    Assert.AreNotEqual(xtensiveVersion, xtensive.VersionInfo);
    Assert.AreNotEqual(alexVersion, alex.VersionInfo);
    xtensiveVersion = xtensive.VersionInfo;
    alexVersion = alex.VersionInfo;

    Console.WriteLine("Dmitri.Company = Xtensive");
    dmitri.Company = xtensive;
    Dump(xtensive);
    Assert.AreNotEqual(xtensiveVersion, xtensive.VersionInfo);
    Assert.AreNotEqual(dmitriVersion, dmitri.VersionInfo);
    xtensiveVersion = xtensive.VersionInfo;
    dmitriVersion = dmitri.VersionInfo;

    Console.WriteLine("Transaction rollback test, before:");
    Dump(xtensive);
    var xtensiveVersionFieldValue = xtensive.Version;
    using (var tx = session.OpenTransaction()) {

      xtensive.Employees.Remove(alex);
      // Xtensive version is changed
      var newXtensiveVersionInsideTransaction = xtensive.VersionInfo;
      Assert.AreNotEqual(xtensiveVersion, newXtensiveVersionInsideTransaction);
      // Incremented
      Assert.AreEqual(xtensiveVersionFieldValue, xtensive.Version - 1);
      // Alex version is changed
      Assert.AreNotEqual(alexVersion, alex.VersionInfo);

      xtensive.Employees.Remove(dmitri);
      // Xtensive version is NOT changed, since we try to update each version
      // just once per transaction
      Assert.AreEqual(
        newXtensiveVersionInsideTransaction, xtensive.VersionInfo);
      // No increment now
      Assert.AreEqual(xtensiveVersionFieldValue, xtensive.Version - 1);
      // Dmitri's version is changed
      Assert.AreNotEqual(dmitriVersion, dmitri.VersionInfo);

      Console.WriteLine("Transaction rollback test, inside:");
      Dump(xtensive);
      // tx.Complete(); // Rollback!
    }

    Console.WriteLine("Transaction rollback test, after:");
    Dump(xtensive);

    // Let's check if everything is rolled back
    Assert.AreEqual(xtensiveVersion, xtensive.VersionInfo);
    Assert.AreEqual(xtensiveVersionFieldValue, xtensive.Version);
    Assert.AreEqual(xtensiveVersion, xtensive.VersionInfo);
    Assert.AreEqual(dmitriVersion, dmitri.VersionInfo);
  }
}

[Test]
public void VersionValidatorTest()
{
  var domain = GetDomain();

  using (var session = domain.OpenSession()) {
    var versions = new VersionSet();

    // Building initial state (in auto transactions)
    var alex = new Person("Yakunin, Alex");
    var dmitri = new Person("Maximov, Dmitri");

    using (VersionCapturer.Attach(versions))
    using (var tx = session.OpenTransaction()) {
      // Simulating entity displaying @ web page
      // By default this leads to their addition to VersionSet
      Dump(alex);
      Dump(dmitri);
      tx.Complete();
    }

    // Let's clone VersionSet (actually - serialize & deserialize)
    versions = Cloner.Default.Clone(versions);
    // And dump it
    Dump(versions);

    using (VersionValidator.Attach(versions))
    using (var tx = session.OpenTransaction()) {
      // Passes
      alex.Name = "Edited Alex";
      // Passes, because this is not the very first modification
      alex.Name = "Edited again";
      tx.Complete();
    }

    AssertEx.Throws<VersionConflictException>(() => {
      using (VersionValidator.Attach(versions))
      using (var tx = session.OpenTransaction()) {
        alex.Name = "And again";
        tx.Complete();
      } // Version check fails on Session.Persist() here
    });

    versions.Add(alex, true); // Overwriting versions
    Dump(versions);

    using (VersionValidator.Attach(versions))
    using (var tx = session.OpenTransaction()) {
      alex.Name = "Edited again"; // Passes now
      tx.Complete();
    }
  }
}

private void Dump(Entity entity)
{
  Console.WriteLine("Entity: {0}", entity);
  Console.WriteLine("          Key: {0}", entity.Key);
  Console.WriteLine("  VersionInfo: {0}", entity.VersionInfo);
  Console.WriteLine();
}

private void Dump(VersionSet versions)
{
  Console.WriteLine("VersionSet: \r\n{0}",
    versions
    .Select(pair => new Pair<Key, VersionInfo>(pair.Key, pair.Value))
    .ToDelimitedString("\r\n")
    .Indent(2, true)
    );
  Console.WriteLine();
}

Logging

Basics

DataObjects.Net’s logging infrastructure consists of 2 main components: loggers and log writers.

Loggers

  • * - special logger, logs everything
  • Xtensive.Orm - logs Session & Transaction-related events and exceptions.
  • Xtensive.Orm.Core - logs misc. internal events. Normally, nobody is interested in them except DataObjects.Net developers.
  • Xtensive.Orm.Building - logs events during the Domain building process.
  • Xtensive.Orm.Sql - logs SQL statements sent to database server.
  • Xtensive.Orm.Upgrade - logs events during database schema upgrade.

Log writers

  • Console - redirects output to application’s console window, if any. Useful for small & test projects.
  • DebugOnlyConsole - the same as Console but appends log data only when a project is run in Debug mode.
  • path_to_file - appends log data to a specified file. If file is absent, it will be created. Useful for development & production environments. path_to_file can be either absolute or relative to the application location.
  • None - writes to /dev/null.

Configuring log output

Configuration of built-in logging is made in application configuration file (app.config or web.config).

Logger configuration takes 2 parameters: logger name as source and log writer name as target.

<Xtensive.Orm>
  <domains>
    <domain name="Default".../>
  </domains>

  <logging>
    <log source="Xtensive.Orm" target="Console"/>
    <log source="Xtensive.Orm.Sql" target="C:\Debug\Sql.log"/>
  </logging>
</Xtensive.Orm>

The example of log:

2012-12-31 00:00:02,052 | DEBUG | Xtensive.Orm.Sql | Session 'Default, #9'. Creating connection 'sqlserver://*****'.
2012-12-31 00:00:02,052 | DEBUG | Xtensive.Orm.Sql | Session 'Default, #9'. Opening connection 'sqlserver://*****'.
2012-12-31 00:00:02,052 | DEBUG | Xtensive.Orm.Sql | Session 'Default, #9'. Beginning transaction @ ReadCommitted.
2012-12-31 00:00:02,068 | DEBUG | Xtensive.Orm.Sql | Session 'Default, #9'. SQL batch: SELECT [a].[Id], [a].[TypeId], [a].[Name], [a].[Code], ...
2012-12-31 00:00:02,068 | DEBUG | Xtensive.Orm.Sql | Session 'Default, #9'. Commit transaction.
2012-12-31 00:00:02,068 | DEBUG | Xtensive.Orm.Sql | Session 'Default, #9'. Closing connection 'sqlserver://*****'.

Built-in services

Some very specific tasks can’t be easily done with the help of public API. For those cases DataObjects.Net provides a set of so-called “internal” services that usually can be obtained through Session.Services endpoint or through static helpers. The services’ API surface is not final and may be changed in the future versions.

Note

Use Xtensive.Orm.Services namespace to refer to the services.

SessionStateAccessor

Exposes methods to internal state of Session.

using Xtensive.Orm.Services;

using (var session = Domain.OpenSession()) {
  using (var tx = session.OpenTransaction()) {
    var sessionAccessor = DirectStateAccessor.Get(session);

    // Number of entities in the cache
    int count = sessionAccessor.Count;

    // List all entities in session cache
    foreach(var entities in sessionAccessor)
      // apply some action

    // Resolve entity from session cache by key
    var entity = sessionAccessor[myKey];

    // Invalidates state of session cache
    sessionAccessor.Invalidate();

    tx.Complete();
  }
}

PersistentStateAccessor

Exposes methods to access internal state of a Persistent object. PersistentFieldState can be of two kinds: Loaded or Modified.

using Xtensive.Orm.Services;

using (var session = Domain.OpenSession()) {
  using (var tx = session.OpenTransaction()) {

    var animal = session.Query.All<Animal>().First();
    animal.Name = "Tiger";

    var accessor = DirectStateAccessor.Get(animal);

    // Checking the state of field
    var state = accessor.GetFieldState("Name");
    // state is PersistentFieldState.Modified

    tx.Complete();
  }
}

EntitySetStateAccessor

Exposes methods to internal state of EntitySet.

using Xtensive.Orm.Services;

using (var session = Domain.OpenSession()) {
  using (var tx = session.OpenTransaction()) {

    var person = session.Query.All<Person>().First();

    var accessor = DirectStateAccessor.Get(person.Pets);

    // Number of keys cached
    long count = accessor.Count;

    // Check whether count is available without query to database
    accessor.IsCountAvailable;

    // Check whether EntitySet is fully loaded from database
    accessor.IsFullyLoaded;

    // Check whether a key is cached or not
    accessor.Contains(myKey);

    // Enumerates all cached keys
    foreach(var key in accessor)
      // apply some action

    tx.Complete();
  }
}

DirectEntityAccessor

Exposes methods for creating instances of Persistent types and accessing their persistent fields.

using Xtensive.Orm.Services;

using (var session = Domain.OpenSession()) {
  using (var tx = session.OpenTransaction()) {

    var accessor = session.Get<DirectEntityAccessor>();

    // Methods for creating entities
    accessor.CreateEntity(typeof(Animal));
    accessor.CreateEntity(typeof(Animal), tuple);
    accessor.CreateEntity(myKey);

    // Methods for creating structures
    accessor.CreateStructure(typeof(Address));
    accessor.CreateStructure(typeof(Address), tuple);

    Animal animal = session.Query.All<Animal>().First();

    // Methods for accessing value of persistent fields
    FieldInfo nameField = Domain.Model.Types[typeof(Animal)].Fields["Name"];
    accessor.GetFieldValue(animal, nameField);
    accessor.SetFieldValue(animal, nameField, "Tiger");

    // Methods for accessing value of reference fields without fetching referenced entity
    FieldInfo ownerField = Domain.Model.Types[typeof(Animal)].Fields["Owner];
    Key ownerKey = accessor.GetReferenceKey(animal, ownerField);
    accessor.SetReferenceKey(animal, ownerField, newKey);

    tx.Complete();
  }
}

DirectEntitySetAccessor

Exposes methods for manipulating fields of EntitySet<T> type in generic way.

using Xtensive.Orm.Services;

using (var session = Domain.OpenSession()) {
  using (var tx = session.OpenTransaction()) {

    var accessor = session.Get<DirectEntitySetAccessor>();

    Person person = session.Query.All<Person>().First();
    FieldInfo petsField = Domain.Model.Types[typeof(Person)].Fields["Pets"];

    // Accessing instance of EntitySet
    accessor.GetEntitySet(person, petsField);

    // Methods for manipulating content of EntitySet
    accessor.Add(person, petsField, myAnimal);
    accessor.Remove(person, petsField, myAnimal);
    accessor.Clear(person, petsField);

    tx.Complete();
  }
}

DirectSqlAccessor

Exposes methods to access low-level objects like connection, command, transaction.

using Xtensive.Orm.Services;

using (var session = domain.OpenSession()) {
  using (var tx = session.OpenTransaction()) {

    var accessor = session.Services.Demand<DirectSqlAccessor>();

    var command = accessor.CreateCommand();
    command.CommandText = "DELETE FROM [dbo].[Animal];";
    command.ExecuteNonQuery();

    // It is a good idea to invalidate session cache after any direct manipulation
    DirectStateAccessor.Get(session).Invalidate();

    t.Complete();
  }
}

QueryFormatter

Provides methods for formatting LINQ queries.

using Xtensive.Orm.Services;

using (var session = Domain.OpenSession()) {
  using (var tx = session.OpenTransaction()) {

    var query = session.Query.All<FakeClass>().Where(f => f.Id > 0);
    var formatter = session.Services.Get<QueryFormatter>();

    // Query is translated to SQL
    Console.WriteLine(formatter.ToSqlString(query));

    // Query is formatted in C# expression notation
    Console.WriteLine(formatter.ToString(query));

    // Creates DbCommand based on query
    var command = formatter.ToDbCommand(query);

    tx.Complete();
  }
}

Customizing LINQ translation

LINQ translator in DataObjects.Net handles wide variety of methods of base class library types, such as String.Substring():

var substrings = session.Query.All<Person>()
  .Select(p => p.Name.Substring(2, 3));

LINQ translation pipeline is not limited by this set of methods. There are two ways of extending it:

  • Implementing custom SQL compilers. Such compilers define translation of a particular method call, property or field access to SQL DOM expression.
  • Implementing custom LINQ expression rewriters. These allow rewriting expressions that use non-persistent properties, methods and fields to expressions that are known to DataObjects.Net LINQ translator.

First of all, you must choose members to create custom compilers for and declare compiler container type. Compiler container is a special static class exposing member compilers as its static methods. It must be marked with [CompilerContainer] attribute:

[CompilerContainer(typeof (Expression))]
public static class CustomLinqCompilerContainer
{
}

Dependently on required compiler type you should pass the following argument to [CompilerContainer] constructor:

  • typeof (Xtensive.Sql.Dml.SqlExpression) to write SQL compiler
  • typeof (System.Linq.Expressions.Expression) for LINQ expression rewriter

Now you can write your own member compilation methods (member compilers) inside it. To define a compilation method (compiler) you need to create a static method and mark it with [Compiler] attribute. Each member compiler matches a single member of a particular type. Each time this member is encountered in LINQ expression corresponding compiler is invoked to provide transformation. Member compiler should have a signature based on the following rules:

  • If target member is a generic method or target member belongs to a generic type MemberInfo parameter should be added. This parameter is used to pass actual member for which this compiler is invoked. It allows determining actual generic parameters which where used to construct a particular generic member.
  • If target member is an instance member the corresponding parameter should be added. This parameter defines expression representing instance which is used for accessing member.
  • If target member is a method or indexed property you should add as many parameters as target member has.
[Compiler(typeof(CustomSqlCompilerStringExtensions),
  "BuildAddressString",
  TargetKind.Method | TargetKind.Static)]
public static SqlExpression BuildAddressString(
  SqlExpression countryExpression,
  SqlExpression streetExpression,
  SqlExpression buildingExpression)

[Compiler] attribute parameters:

  • Type where member is defined.
  • Name of the member
  • Member kind: static or instance; method, property or field an so on.

You can (but not required to) apply [Type] attribute to any argument in a compiler method declaration. When [Type] attributes are provided, compiler resolver will consider argument types specified there during the matching process. Such declarations allow creating different compilers for different overloads of the same method. If there are no overloaded methods or overloading is based solely on number of parameters such declarations are not required.

Finally, you must register compiler container in the domain configuration:

var config = new DomainConfiguration("sqlserver://localhost/DO40-Tests");
config.Types.Register(typeof (CustomLinqCompilerContainer));

If all these steps are completed, DataObjects.Net will use your compiler container in the corresponding domain.

Customizing compilation to SQL

Result of compilation of any .NET expression to SQL is SqlExpression object from Xtensive.Sql.Dml namespace. SQL compilers are dealing with internal abstractions (Tuple field values), so there are no entities, structures and other high-level objects. The only types you can deal with are primitive types supported by DataObjects.Net, such as String and DateTime.

To illustrate the usage of custom SQL compiler, let’s imagine we want to extend string type with GetThirdChar() method returning 3rd character in the string.

First of all, we need this method itself:

public static class CustomCompilerStringExtensions
{
  public static char GetThirdChar(this string source)
  {
    return source[2];
  }
}

A query using it:

var thirdChars = session.Query.All<Person>().Select(p => p.Name.GetThirdChar());

Let’s write its compiler:

[CompilerContainer(typeof(SqlExpression))]
public static class CustomStringCompilerContainer
{
  [Compiler(typeof (CustomCompilerStringExtensions),
    "GetThirdChar", TargetKind.Method | TargetKind.Static)]
  public static SqlExpression GetThirdChar(SqlExpression _this)
  {
    return SqlDml.Substring(_this, 2, 1);
  }
}

If this compiler is registered in the domain, SQL DOM translator will convert any GetThirdChar() method call to SqlDml.Substring(_this, 2, 1) expression.

Let’s imagine another case: we want to build address string from its components:

public static string BuildAddressString(
    string country, string city, string building)
{
  return string.Format("{0}, {1}-{2}", country, city, building);
}

The compiler for this method:

[Compiler(typeof(CustomSqlCompilerStringExtensions),
  "BuildAddressString",
  TargetKind.Method | TargetKind.Static)]
public static SqlExpression BuildAddressString(
  SqlExpression countryExpression,
  SqlExpression streetExpression,
  SqlExpression buildingExpression)
{
  return SqlDml.Concat(
    countryExpression, SqlDml.Literal(", "),
    streetExpression, SqlDml.Literal("-"), buildingExpression);
}

Now this method can be used in LINQ queries:

var addresses = session.Query.All<Person>().Select(p =>
  CustomSqlCompilerStringExtensions.BuildAddressString(
  p.Address.Country, p.Address.City, p.Address.Building));

Finally, let’s try to create a compiler for built-in GetHashCode() method of string type. Since method is built-in, there should be just its compiler:

[Compiler(typeof(string), "GetHashCode", TargetKind.Method)]
public static SqlExpression GetHashCode(SqlExpression _this)
{
  // Return string length as its hash code
  return SqlDml.CharLength(_this);
}

Note that [Compiler] attribute uses typeof(string) as its first parameter, because GetHashCode() method we’re going to compile is overriden in string type.

This compiler makes possible to run the following query:

var hashCodes = session.Query.All<Person>()
  .OrderBy(p=>p.Id)
  .Select(p => p.Address.Country.GetHashCode());

Writing custom LINQ expression rewriter

LINQ expression rewriters provide much more convenient way of extending the LINQ translator:

  • SQL compilation is one of final steps in LINQ translation pipeline. So the abstractions you deal with (or return) here are SQL DOM primitives operating with primitive data types.
  • But LINQ rewriters operate on the first stage of translation, so all the expressions from original LINQ query are available there. Moreover, you can use (e.g. return) any expressions supported by DataObjects.Net LINQ translator here.

LINQ rewriters translate expressions to expressions, so you must apply [CompilerContainer(Expression)] attribute to a rewriter type and use Expression as common argument & result type in its methods.

[CompilerContainer(typeof (Expression))]
public static class CustomLinqCompilerContainer

Let’s extend persistent Person type with non-persistent FullName property:

public string FullName
{
  get { return string.Format("{0} {1}", FirstName, LastName); }
}

Its LINQ rewriter:

[Compiler(typeof (Person), "FullName", TargetKind.PropertyGet)]
public static Expression FullName(Expression personExpression)
{
  var spaceExpression = Expression.Constant(" ");
  var firstNameExpression = Expression.Property(personExpression, "FirstName");
  var lastNameExpression = Expression.Property(personExpression, "lastName");
  var methodInfo = typeof (string).GetMethod("Concat",
    new[] {typeof (string), typeof (string), typeof (string)});
  var concatExpression = Expression.Call(
    Expression.Constant(null, typeof(string)),
    methodInfo, firstNameExpression, spaceExpression, lastNameExpression);
  return concatExpression;
}

There is a way to reduce rewriter’s complexity – we can create LambdaExpression and bind its parameters:

[Compiler(typeof (Person), "FullName", TargetKind.PropertyGet)]
public static Expression FullName(Expression personExpression)
{
  // Since "ex" type is specified, C# compiler
  // allows to use Person properties:
  Expression<Func<Person, string>> ex =
    person => person.FirstName + " " + person.LastName;

  // Binding lambda parameters replaces parameter usage in lambda.
  // In this case resulting expression body looks like this:
  // personExpression.FirstName + " " + personExpression.LastName
  return ex.BindParameters(personExpression);
}

As you already know, we must register this rewriter:

var config = new DomainConfiguration("sqlserver://localhost/DO40-Tests");
config.Types.Register(typeof (CustomLinqCompilerContainer));

Now FullName property can be used in LINQ queries:

var fullNames = session.Query.All<Person>()
  .OrderBy(p => p.Id)
  .Select(p => p.FullName);

Now let’s imagine we need a method addding custom prefix to LastName property:

public string PrefixLastName(string prefix)
{
  return string.Format("{0}{1}", prefix, LastName);
}

Custom compiler for this method:

[Compiler(typeof (Person), "PrefixLastName", TargetKind.Method)]
public static Expression PrefixLastName(
  Expression personExpression, Expression prefixExpression)
{
  Expression<Func<Person, string, string>> ex
    = (person, prefix) => prefix + person.LastName;
  return ex.BindParameters(personExpression, prefixExpression);
}

Now it’s possible to use PrefixLastName method in LINQ queries:

var resultStrings = session.Query.All<Person>()
  .OrderBy(p => p.Id)
  .Select(p => p.PrefixLastName("Mr. "));

var resultStrings = session.Query.All<Person>()
  .OrderBy(p => p.Id)
  .Select(p => p.PrefixLastName(p.Id.ToString()));

All custom compilers must be deterministic: they should produce the same resulting expression for the same input. If this condition is violated, the query involving such a compiler will behave improperly while being used inside Session.Query.Execute() method. This does not mean the result of evaluation of returned expression must be deterministic as well. So you can use e.g. DateTime.Now (non-deterministic function) there – but you must ensure the expression you return is the same for the same input.


Extensions

Overview

DataObjects.Net Extensions are small projects that extend standard functionality of DataObjects.Net core. They are maintained by Xtensive engineers and volunteers from DataObjects.Net community. Each extension has a corresponding NuGet package so they can be installed separately or in any combination. Using NuGet is a recommended way of installing the extensions.

The extensions are published on CodePlex with source code open.

Bulk Operations

The extension provides a set of IQueryable extension methods that are translated to server-side UPDATE or DELETE commands.

How to use

Add Bulk Operations package to your project.

Demo

  1. Update primitive property with a constant value, e.g.:
Query.All<Bar>()
  .Where(a => a.Id == 1)
  .Set(a => a.Count, 2)
  .Update();
  2. Updating persistent property with expression, computed on server, e.g.:
Query.All<Bar>()
  .Where(a => a.Id==1)
  .Set(a => a.Count, a => a.Description.Length)
  .Update();
  3. Setting a reference to an entity that is already loaded into current Session
// Emulating entity loading
var bar = Query.Single<Bar>(1);

Query.All<Foo>()
  .Where(a => a.Id == 2)
  .Set(a => a.Bar, bar)
  .Update();
  4. Setting a reference to an entity that is not loaded into Session, 1-st way
Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Set(a => a.Bar, a => Query.Single<Bar>(1))
  .Update();
  5. Setting a reference to an entity that is not loaded into Session, 2-nd way
Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Set(a => a.Bar, a => Query.All<Bar>().Single(b => b.Name == "test"))
  .Update();
  6. Constructing update expressions of the fly
bool condition = CheckCondition();
var query = Query.All()<Bar>
  .Where(a => a.Id == 1)
  .Set(a => a.Count, 2);

if(condition)
  query = query.Set(a => a.Name, a => a.Name + "test");
query.Update();
  7. Updating lots of properties at once
Query.All<Bar>()
  .Where(a => a.Id == 1)
  .Update(a => new Bar(null) { Count = 2, Name = a.Name + "test", dozens of other properties... });
  8. Deleting entities
Query.All<Foo>()
  .Where(a => a.Id == 1)
  .Delete();

Localization/Internationalization

The extension transparently solves a task of application or service localization. This implies that localizable resources are a part of domain model so they are stored in database.

How to use

  1. Add Localization package to your project.
  2. Include types from Xtensive.Orm.Localization assembly into the domain:
<Xtensive.Orm>
  <domains>
    <domain ... >
      <types>
        <add assembly="your assembly"/>
        <add assembly="Xtensive.Orm.Localization"/>
      </types>
    </domain>
  </domains>
</Xtensive.Orm>
  3. Implement ILocalizable<TLocalization> on your localizable entities, e.g.:
[HierarchyRoot]
public class Page : Entity, ILocalizable<PageLocalization>
{
  [Field, Key]
  public int Id { get; private set; }

  // Localizable field. Note that it is non-persistent
  public string Title
  {
    get { return Localizations.Current.Title; }
    set { Localizations.Current.Title = value; }
  }

  [Field] // This is a storage of all localizations for Page class
  public LocalizationSet<PageLocalization> Localizations { get; private set; }

  public Page(Session session) : base(session) {}
}
  4. Define corresponding localizations, e.g.:
[HierarchyRoot]
public class PageLocalization : Localization<Page>
{
  [Field(Length = 100)]
  public string Title { get; set; }

  public PageLocalization(Session session, CultureInfo culture, Page target)
    : base(session, culture, target) {}
}

Demo

  1. Access localizable properties as regular ones, e.g.:
page.Title = "Welcome";
string title = page.Title;
  2. Mass editing of localizable properties:
var en = new CultureInfo("en-US");
var sp = new CultureInfo("es-ES");
var page = new Page(session);
page.Localizations[en].Title = "Welcome";
page.Localizations[sp].Title = "Bienvenido";
  3. Value of localizable properties reflects culture of the current Thread, e.g.:
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
string title = page.Title; // title is "Welcome"

Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
string title = page.Title; // title is "Bienvenido"
  4. Instead of altering CurrentThread, instance of LocalizationScope can be used, e.g.:
using (new LocalizationScope(new CultureInfo("en-US"))) {
  string title = page.Title; // title is "Welcome"
}

using (new LocalizationScope(new CultureInfo("es-ES"))) {
  string title = page.Title; // title is "Bienvenido"
}
  5. LINQ queries that include localizable properties are transparently translated
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
var query = from p in session.Query.All<Page>()
  where p.Title=="Welcome"
  select p;
Assert.AreEqual(1, query.Count());

Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
var query = from p in session.Query.All<Page>()
  where p.Title=="Bienvenido"
  select p;
Assert.AreEqual(1, query.Count());

Reprocessing

The extension provides API for reprocessible operations. The reprocessible operation should represent a separate block of logic, usually a delegate of a method and be transactional.

How to use

  1. Add Reprocessing package to your project.

Demo

  1. Simple reprocessible operation looks like this:
Domain.Execute(session =>
  {
    // Task logic
  });
  2. There are 3 strategies that can be used for task execution:
  • HandleReprocessibleException strategy
    The strategy catches all reprocessible expections (deadlock and transaction serialization exceptions) and makes another attempt to execute the task
  • HandleUniqueConstraintViolation strategy
    The same as previous one but also catches unique constraint violation exception
  • NoReprocess strategy
    No reprocessing is provided

To indicate that a particular strategy should be used, use the following syntax:

Domain.WithStrategy(new HandleReprocessExceptionStrategy())
    .Execute(session =>
        {
          // Task logic
        });

3. To omit setting up the strategy each time consider configuring it in application configuration file, e.g.:

<configSections>
  ...
  <section name="Xtensive.Orm.Reprocessing"
    type="Xtensive.Orm.Reprocessing.Configuration.ConfigurationSection, Xtensive.Orm.Reprocessing" />
</configSections>

<Xtensive.Orm.Reprocessing
  defaultTransactionOpenMode="New"
  defaultExecuteStrategy="Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing">
</Xtensive.Orm.Reprocessing>

Having that done, in scenarios with no strategy specified, the extension will automatically use the strategy from the configuration.

Security

The extension provides security layer (authentication services, principals, roles, secured queries) There are 2 main parts that can also be used separately: authentication services and role-based access to domain entities

How to use

  1. Add Security package to your project.
  2. Include types from Xtensive.Orm.Security assembly into the domain:
<Xtensive.Orm>
  <domains>
    <domain ... >
      <types>
        <add assembly="your assembly"/>
        <add assembly="Xtensive.Orm.Security"/>
      </types>
    </domain>
  </domains>
</Xtensive.Orm>
  3. If you are planning to use one of authentication services add
<section name="Xtensive.Orm.Security" type="Xtensive.Orm.Security.Configuration.ConfigurationSection,
Xtensive.Orm.Security" />

and set up the desired hashing service:

<Xtensive.Orm.Security>
  <hashingService name="plain"/>
  <!-- other options are: md5, sha1, sha256, sha384, sha512 -->
</Xtensive.Orm.Security>

Demo

  1. Define a class that inherits abstract GenericPrincipal class that will describe your users, e.g.:
[HierarchyRoot]
public class User : GenericPrincipal
{
  [Field, Key]
  public int Id { get; private set; }

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

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

  ...

  public User(Session session) : base(session) {}
}
  2. Having the User class defined, it can be used for user creation and authentication.
// Creating a user
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var user = new User(session);
    user.Name = "admin";
    user.SetPassword("password");
    transaction.Complete();
  }
}

// Authenticating a user
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var user = session.Authenticate("admin", "password");
    transaction.Complete();
  }
}
  3. Define a hierarchy of roles. A role is a set of permissions for a job function within a company, e.g.:
EmployeeRole
|
|- StockManagerRole
|
|- SalesRepresentativeRole
   |
   |- SalesManagerRole
   |
   |- SalesPresidentRole

// This is base role for all employees
[HierarchyRoot(InheritanceSchema = InheritanceSchema.SingleTable)]
public abstract class EmployeeRole : Role
{
  [Field, Key]
  public int Id { get; set; }

  protected override void RegisterPermissions()
  {
    // All employees can read products
    RegisterPermission(new Permission<Product>());
    // All employees can read other employees
    RegisterPermission(new Permission<Employee>());
  }

  protected EmployeeRole(Session session)
    : base(session) {}
}

public class StockManagerRole : EmployeeRole
{
  protected override void RegisterPermissions()
  {
    // Stock manager inherits Employee permissions
    base.RegisterPermissions();

    // Stock manager can read and write products
    RegisterPermission(new Permission<Product>(canWrite:true));
  }

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

// Create instances of roles on first domain initialization
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    new SalesRepresentativeRole(session);
    new SalesManagerRole(session);
    new SalesPresidentRole(session);
    new StockManagerRole(session);
    transaction.Complete();
  }
}
  4. Members of staff are assigned particular roles, e.g.:
using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var stockManagerRole = session.Query.All<StockManagerRole>().Single();
    var user = new User(session);
    user.Name = "peter";
    user.SetPassword("password");
    user.Roles.Add(stockManagerRole);
    transaction.Complete();
  }
}
  5. Checking whether a user has the required role
user.IsInRole("StockManagerRole");
// or
user.Roles.Contains(stockManagerRole);
  6. Session impersonation
using (var imContext = session.Impersonate(user)) {
  // inside the region the session is impersonated with the specified
  // principal and set of their roles and permissions

  // Checking whether the user has a permission for reading Customer entities
  imContext.Permissions.Contains<Permission<Customer>>(p => p.CanRead);

  // Checking whether the user has a permission for writing to Customer entities
  imContext.Permissions.Contains<Permission<Customer>>(p => p.CanWrite);

  // another way
  var p = imContext.Permissions.Get<Permission<Customer>>();
  if (p != null && p.CanRead)
    // allow doing some stuff
}

To end the impersonation call ImpersonationContext.Undo() or Dispose() method.

Impersonation contexts can be nested, e.g.:

using (var userContext = session.Impersonate(user)) {
  // do some user-related stuff

  using (var adminContext = session.Impersonate(admin)) {
    // do some admin stuff
  }

  // we are still in user impersonation context
}
// no context here

7. Secure (restrictive) queries
A role may set up a condition that will be automatically added to any query and filters the query results, e.g.:

public class AutomobileManagerRole : EmployeeRole
{
  private static IQueryable<Customer> GetCustomers(ImpersonationContext context, QueryEndpoint query)
  {
    return query.All<Customer>()
      .Where(customer => customer.IsAutomobileIndustry);
  }

  protected override void RegisterPermissions()
  {
    base.RegisterPermissions();
    // This permission tells that a principal can read/write customers
    // but only those that are returned by the specified condition
    RegisterPermission(new CustomerPermission(true, GetCustomers));
  }

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

Now all employees that have AutomobileManagerRole will read customers that have IsAutomobileIndustry property set to true, e.g.:

using (var session = Domain.OpenSession()) {
  using (var transaction = session.OpenTransaction()) {
    var automobileManagerRole = session.Query.All<AutomobileManagerRole>().Single();
    var user = new User(session);
    user.Name = "peter";
    user.SetPassword("password");
    user.Roles.Add(automobileManagerRole);

    using (var context = session.Impersonate(user)) {
      var customers = Query.All<Customer>();
      // Inside the impersonation context the above-mentioned query condition
      // will be added automatically so user will get only automobile customers
    }
    transaction.Complete();
  }
}

Change Tracking/Auditing

The extension provides tracking/auditing funtionality on Session/Domain level.

How to use

  1. Add Tracking package to your project.
  2. Include types from Xtensive.Orm.Tracking assembly into the domain:
<Xtensive.Orm>
  <domains>
    <domain ... >
      <types>
        <add assembly="your assembly"/>
        <add assembly="Xtensive.Orm.Tracking"/>
      </types>
    </domain>
  </domains>
</Xtensive.Orm>
  3. To track changes on Session level obtain an instance of ISessionTrackingMonitor through Session.Services.Get<ISessionTrackingMonitor>() method. To track changes on Domain level (from all sessions) obtain an instance of IDomainTrackingMonitor through Domain.Services.Get<IDomainTrackingMonitor>() method.

4. Subscribe to TrackingCompleted event. After each tracked transaction is committed you receive the TrackingCompletedEventArgs object.

5. TrackingCompletedEventArgs.Changes contains a collection of ITrackingItem objects, each of them represents a set of changes that occurred to an Entity within the transaction committed.

Demo

  1. Subscribe to ISessionTrackingMonitor/IDomainTrackingMonitor TrackingCompleted event
var monitor = Domain.Services.Get<IDomainTrackingMonitor>();
monitor.TrackingCompleted += TrackingCompletedListener;
  2. Do some changes to persistent entities
using (var session = Domain.OpenSession()) {
 using (var t = session.OpenTransaction()) {
   var e = new MyEntity(session);
   e.Text = "some text";
   t.Complete();
 }
}
  3. Handle TrackingCompleted event call and do whatever you want with tracked changes.
private void TrackingCompletedListener(object sender, TrackingCompletedEventArgs e)
{
 foreach (var change in e.Changes) {
   Console.WriteLine(change.Key);
   Console.WriteLine(change.State);

   foreach (var value in change.ChangedValues) {
     Console.WriteLine(value.Field.Name);
     Console.WriteLine(value.OriginalValue);
     Console.WriteLine(value.NewValue);
   }
 }
}

Web

The extension adds integration for DataObjects.Net and ASP.NET. It contains SessionManager class which is an implementation of IHttpModule and automatically provides Session and transaction for each web request.

SessionManager has the following features:

  1. When Session.Current is being accessed, and there is no current Session, it will provide a new instance of Session. In this case a new transaction will be created. It will be committed on successful completion of http request, otherwise it will be rolled back.
  2. Setting SessionManager.Demand().Error to true will lead to rollback of this transaction.
  3. SessionManager.Current (and SessionManager.Demand()) returns the instance of SessionManager bound to the current HttpContext, i.e. current SessionManager. Its Session property (if not null) is the same value as the one provided by Session.Current.

Note that presence of SessionManager does not prevent you from creating Sessions manually. It operates relying on Session.Resolver event, which is raised only when there is no current Session.

Finally, no automatic Session + transaction will be provided, if you don’t use Session.Current/Session.Demand() methods in your code (directly or indirectly). So e.g. requests to static web pages won’t lead to any DB interaction.

How to use

  1. Add Web package to your project.
  2. Register SessionManager in HttpModulesSection in web.config file.

web.config:

<configuration>
  <system.web>
    <httpModules>
      <add name="SessionManager" type="Xtensive.Orm.Web.SessionManager, Xtensive.Orm.Web"/>
    </httpModules>
  </system.web>
</configuration>
  2. Set up SessionManager.DomainBuilder property in Application_Start method of your Global.asax.cs file.
using Xtensive.Orm.Web;

public class Global : System.Web.HttpApplication
{
  protected void Application_Start(object sender, EventArgs e)
  {
    SessionManager.DomainBuilder = DomainBuilder.Build;
  }
}

Demo

public partial class EditCustomer : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    // Session is provided automatically, transaction also starts
    var session = Session.Demand();
    var id = Request["customerId"];
    if (!string.IsNullOrEmpty(id)) {
      var customerId = int.Parse(id);
      var customer = session.Query.Single<Customer>(customerId);
    }
    ...
  }

  protected void Save(object sender, EventArgs e)
  {
    try {
      var session = Session.Demand();
      if (customer==null)
        customer = new Customer(session);
      customer.Name = textName.Text;
      ...
      Back();
    }
    catch(InvalidOperationException exception) {
      // This will roll back the transaction on end of http request
      SessionManager.Current.HasErrors = true;
    }
  }


  • previous
  • DataObjects.Net Manual »

Epilogue

This is the end of DataObjects.Net Manual. We hope you’ll enjoy using our framework. Please don’t hesitate leaving us a note related to any issues you’ve faced. Below is the list of resources that might be helpful for you further:

  • Product homepage
  • Download page
  • Manual and API Reference, online version
  • Online support service
  • DataObjects.Net team blog

  • previous