Transaction Awareness Framework
nuget package: https://www.nuget.org/packages/CompileTimeAOP.TransactionAwareness/Transaction Awareness Framework is implemented by CompileTimeWeaver.Fody to gain the best performance. It removes the burden of NHibernate developers of managing sessions and transactions, and domain model validations.
1. Transaction-aware Object
A Transaction-aware Object is a .net object that
automatically execute within the scope of a NHibernate session, it participates
in an existing or ongoing session, or be the root of a new session. Similar to
enterprise component in .net or session bean in EJB, a Transaction-aware Object
is a business logic object accessing domain objects (or entity objects). The
declarative transaction aware attribute specifies how an object participates in
a session, and is configured programmatically. The following example shows a Transaction-aware c# class:
[TransactionAware]
public class MyClass: IDisposable
{
public void TravelAllPersons()
{
var session = TransactionContext.CurrentSession;
var persons = session.Query<Person>();
foreach (var person in persons)
{
Trace.WriteLine(person.Id + " - " + person.FirstName);
}
}
public void Dispose()
{
//do nothing
}
}
You see there is nothing special except the C# class is decorated by TransactionAwareAttribute.
[TransactionAware]
public class MyClass: IDisposable
{
public void TravelAllPersons()
{
var session = TransactionContext.CurrentSession;
var persons = session.Query<Person>();
foreach (var person in persons)
{
Trace.WriteLine(person.Id + " - " + person.FirstName);
}
}
public void Dispose()
{
//do nothing
}
}
You see there is nothing special except the C# class is decorated by TransactionAwareAttribute.
2. Context Session
A transaction-aware object is always in a
context session, no matter it is newly created or is propagated from caller.
§ Owner of context session
The transaction-aware object that creates new
context session is the owner of the context session.
§ Transaction-aware methods
Transaction-aware methods are non-static methods decorated by TransactionAwareAttribute, or the non-static methods of a class decorated by TransactionAwareAttribute. Transaction-aware methods can enter session
context and access context session.
§ Life-cycle of context session
A context session is created for the owner
when the owner is created or when first time a transaction-aware method of its
owner is called, depending on TransactionAwarePoint, and it doesn't disappear
until its owner is disposed. (see TransactionAwarePoint below)
§ Attaching moment
If TransactionAwarePoint is OnCall, a
transaction-aware object is attached to a context session when first time any
one of its transaction-aware methods is called from any thread. If
TransactionAwarePoint is OnConstructor, the object is attached to a context
session when it is created. The attaching relation doesn't break or change once
established.
A transaction-aware object can access context
session with static property TransactionContext.CurrentSession or static method TransactionContext.GetCurrentSession(string dbname), and then take
full advantage of NHibernate powers to access database, but a transaction-aware
object is not allowed to close context session explicitly. Below is correct
code snippet to access database through context session:
ISession session = TransactionContext.CurrentSession;
var user1 = session.Get<User>(1);
user1.Name = "Simon Lu";
session.Update(users);
3. Requirements of a Transaction-aware Object
4. Object Injection
5. Voting in an Automatic Transaction
var user1 = session.Get<User>(1);
user1.Name = "Simon Lu";
session.Update(users);
3. Requirements of a Transaction-aware Object
A transaction-aware object is an instance of an
ordinary .net class meeting of few simple requirements:
§ Decorated with TransactionAwareAttribute.
§ Implement IDisposable interface, this method must exist event though it is
empty.
§ Call TransactionContext methods/properties in
transaction-aware methods only.
When applying the transaction attribute, you
can set TransactionOption property and TransactionAwarePoint property.
The following table lists and describes each constructor variation.
TransactionOption
|
Description
|
Required (default)
|
Indicates that the object requires
a session. It runs in the scope of an existing session, if one exists. If no
session exists, the object starts one.
|
RequiresNew
|
Indicates that the object requires
a session and a new session is started for each request.
|
TransactionAwarePoint
|
Description
|
OnCall(default)
|
Indicates that the object attaches
to a session when any of its virtual method is first called.
|
OnConstructor
|
Indicates that the object attaches
to a session right after it is created.
|
Example:
[TransactionAware(TransactionAwareOption = TransactionAwareOption.Required,
TransactionAwarePoint = TransactionAwarePoint.OnConstructor)]
public class MyClass: IDisposable
{
//...
}
4. Object Injection
A Transaction-aware object can inject objects as
its properties or fields right after it is created by declare the
properties/fields with InjectedOnCreatedAttribute, and transparently dispose
them when it is disposed. This open the door for developers to inject Transaction-aware objects into the other Transaction-aware object so that they can share the same database session. Below is an example class, and instances of EntityRepository is created and
injected as private field _entityRepository, and it is disposed automatically even though the Dispose method body is empty.
[TransactionAware]
public class TransactionAwareUnitOfWork : IDisposable
{
[InjectedOnCreated]
private EntityRepository _entityRepository;
...
public void Dispose()
{
//Transaction Awareness framework will dispose all injected objects
}
}
5. Voting in an Automatic Transaction
Transaction-aware methods of transaction-aware
objects can vote to commit or abort their current transaction. The absence of
an explicit vote in a method means the database update in the method accumulate
the context transaction without commit or rollback. If there is not explicit
vote during the life-cycle of the owner of a context session, the context
transaction is rollback when the owner is disposed, other than that, a context
transaction is neither committed nor aborted until a transaction-aware method
explicitly vote.
Explicit voting also allows your class to abort
a transaction if it encounters a significant error. Again, you can improve your
application's performance by catching a fatal error early in the transaction,
ending the transaction, and releasing resources. There are two options to issue
explicit voting: SetComplete/SetAbort or AutoCompleteAttribute.
Using SetAbort and
SetComplete
You can use TransactionContext class, which
exposes the SetComplete or SetAbort methods, to explicitly commit or abort a
transaction. SetComplete indicates that your object votes to commit its work;
SetAbort indicates that your object encountered a problem and votes to abort the
ongoing transaction, a single abort vote from any object participating in the
transaction causes the entire transaction to fail.
The following code fragment shows the SetAbort
and SetComplete methods in use.
//Try
to do something crucial to the transaction in progress.
if(
!DoSomeWork() )
{
//Something goes wrong.
TransactionContext.SetAbort();
}
else
{
//All goes well.
TransactionContext.SetComplete();
}
Using AutoComplete
The AutoCompleteAttribute causes a transaction-aware
method participating in a transaction to vote in favor of completing the
transaction if the method returns normally. If the method call throws an
exception, the transaction is aborted. To use this feature, insert the
attribute before the class method. If you add the attribute to an interface
method, the common language run-time ignores it. The following code fragment
shows the placement of the attribute on a transaction-aware class.
[TransactionAware]
public
class Account
{
[AutoComplete]
public virtual void Debit(int amount)
{
// Do some database work. Any exception
thrown here aborts the transaction;
// otherwise, transaction commits.
}
}
6. Practice
Prerequisite: domain classes and mapping classes are ready.
Step 2) Add a Transaction-aware class to your project
using CompileTimeAOP.Transaction;
[TransactionAware]
public class MyClass: IDisposable
{
public void TravelAllPersons()
{
var session = TransactionContext.CurrentSession;
var persons = session.Query<Person>();
foreach (var person in persons)
{
Trace.WriteLine(person.Id + " - " + person.FirstName);
}
}
public void Dispose()
{
//do nothing
}
}
Step 3) Add this code snippet into application startup code to setup database connection
using CompileTimeAOP.Transaction;
DbSessionFactory.Create("your_database_name",
"hibernate.yourdb.config",
fluentCfg =>
{
fluentCfg.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<YourFluentNHibernateMapClass>();
});
fluentCfg.ExposeConfiguration(cfg => new NHibernate.Tool.hbm2ddl.SchemaUpdate(cfg).Execute(false, true));
});
Step 4) Use Transaction-aware class
using (var testObj = new MyClass())
{
testObj.TravelAllPersons();
}
Step 2) Add a Transaction-aware class to your project
using CompileTimeAOP.Transaction;
[TransactionAware]
public class MyClass: IDisposable
var persons = session.Query<Person>();
public void Dispose()
DbSessionFactory.Create("your_database_name",
"hibernate.yourdb.config",
fluentCfg =>
{
fluentCfg.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<YourFluentNHibernateMapClass>();
});
fluentCfg.ExposeConfiguration(cfg => new NHibernate.Tool.hbm2ddl.SchemaUpdate(cfg).Execute(false, true));
});
{
testObj.TravelAllPersons();
}
using CompileTimeAOP.Transaction;
[TransactionAware]
public class MyClass: IDisposable
{
public void TravelAllPersons()
{
var session = TransactionContext.CurrentSession;
var persons = session.Query<Person>();
foreach (var person in persons)
{
Trace.WriteLine(person.Id + " - " + person.FirstName);
}
}
public void Dispose()
{
//do nothing
}
}
Step 3) Add this code snippet into application startup code to setup database connection
using CompileTimeAOP.Transaction;
"hibernate.yourdb.config",
fluentCfg =>
{
fluentCfg.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<YourFluentNHibernateMapClass>();
});
fluentCfg.ExposeConfiguration(cfg => new NHibernate.Tool.hbm2ddl.SchemaUpdate(cfg).Execute(false, true));
});
Step 4) Use Transaction-aware class
using (var testObj = new MyClass()){
testObj.TravelAllPersons();
}
7. Multiple Database
Sometimes it is necessary for a Transaction-aware object to access two databases in one method, for example, an online shopping cart object updates transaction database and access customer service database in its PlaceOrder () method. Transaction-awareness framework let it do it easily by decorating the PlaceOrder method with two TransactionAwareAttribute with different database names as constructor parameter. See it below:
[TransactionAware("TransactionDb"]
public class OnlineShoppingStore: IDisposable
{
[TransactionAware("CustomerDb"]
[AutoComplete]
public void PlaceOrder(OrderDto order)
{
var customerDbSession = TransactionContext.GetCurrentSession("CustomerDb");
var customer = customerDbSession.Query<CustomerEntity>(order.CustomerId);
...
var transDbSession = TransactionContext.GetCurrentSession("TransactionDb");
transDbSession.Save(new OrderEntity(){...});
}
...
}
Notes:
- use TransactionContext.GetCurrentSession(string dbName) to get database sessions from different database where multiple databases exist.
- Call DbSessionFactory.Create() for each database with different database name to setup connection.
[TransactionAware("TransactionDb"]
public class OnlineShoppingStore: IDisposable
{
[TransactionAware("CustomerDb"]
[AutoComplete]
public void PlaceOrder(OrderDto order)
{
var customerDbSession = TransactionContext.GetCurrentSession("CustomerDb");
var customer = customerDbSession.Query<CustomerEntity>(order.CustomerId);
...
var transDbSession = TransactionContext.GetCurrentSession("TransactionDb");
transDbSession.Save(new OrderEntity(){...});
}
...
}
Notes:
- use TransactionContext.GetCurrentSession(string dbName) to get database sessions from different database where multiple databases exist.
- Call DbSessionFactory.Create() for each database with different database name to setup connection.
8. Fluent Validation
Transaction-aware framework uses FluentValidation to keep the integrity of data model whenever there is database update.
Transaction-aware framework uses FluentValidation to keep the integrity of data model whenever there is database update.
Usage:
- Create a C# project to contain all the FluentValidation validators.
- Add all validators to validate entity classes, such as the PersonValidator class below to validate Person entity class:
using FluentValidation;
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(x => x.Id).NotEmpty();
RuleFor(x => x.FirstName).NotNull().Length(1, 10);
}
}
- Add validator assembly to the call to DbSessionFactory.Create() as the last parameter, the framework validates entity objects once there is update on them, and throws FluentValidation.ValidationException exception if there is any break to validation rules.
DbSessionFactory.Create("your_database_name",
"hibernate.yourdb.config",
fluentCfg =>
{
fluentCfg.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<YourFluentNHibernateMapClass>();
});
fluentCfg.ExposeConfiguration(cfg => new NHibernate.Tool.hbm2ddl.SchemaUpdate(cfg).Execute(false, true));
},
typeof (PersonValidator).Assembly);
Usage:
- Create a C# project to contain all the FluentValidation validators.
- Add all validators to validate entity classes, such as the PersonValidator class below to validate Person entity class:
using FluentValidation;
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(x => x.Id).NotEmpty();
RuleFor(x => x.FirstName).NotNull().Length(1, 10);
}
}
- Add validator assembly to the call to DbSessionFactory.Create() as the last parameter, the framework validates entity objects once there is update on them, and throws FluentValidation.ValidationException exception if there is any break to validation rules.
DbSessionFactory.Create("your_database_name",
"hibernate.yourdb.config",
fluentCfg =>
{
fluentCfg.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<YourFluentNHibernateMapClass>();
});
fluentCfg.ExposeConfiguration(cfg => new NHibernate.Tool.hbm2ddl.SchemaUpdate(cfg).Execute(false, true));
},
typeof (PersonValidator).Assembly);
No comments:
Post a Comment