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.
DataObjects.Net supports both optimistic and pessimistic concurrency control:
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).
DataObjects.Net’s logging infrastructure consists of 2 main components: loggers and log writers.
Loggers
Log writers
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://*****'.
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.
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();
}
}
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();
}
}
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();
}
}
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();
}
}
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();
}
}
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();
}
}
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();
}
}
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:
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:
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:
[Compiler(typeof(CustomSqlCompilerStringExtensions),
"BuildAddressString",
TargetKind.Method | TargetKind.Static)]
public static SqlExpression BuildAddressString(
SqlExpression countryExpression,
SqlExpression streetExpression,
SqlExpression buildingExpression)
[Compiler] attribute parameters:
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.
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());
LINQ expression rewriters provide much more convenient way of extending the LINQ translator:
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.