Dislcaimer: This post is going to be long.
The unit of work implementation in 1.0 worked, and worked quite well. There was no real need to refactor it for 1.1, or any major issues with how it worked. But that being said, while adding multi-database support, more on this later, I started to realize some code smells in there. Coupled that with the kind of questions I’ve gotten on clarifications on what UnitOfWork is and why or when to use a UnitOfWorkScope, I went back and took a long and hard look at the current implementation.
Lets start with the questions first…
Q1: How do I start a UnitOfWork?
Okay this should be a simple question to answer, but it isn’t. The reason being there are TWO ways to start a unit of work in NCommon. You can manually start a unit of work by using the static Start method on the UnitOfWork class:
104 using(var uow = UnitOfWork.Start())
105 using (var uowTx = uow.BeginTransaction())
106 {
107
108 //do something here
109 uow.Flush();
110 uowTx.Commit();
111 }
So the Start method would return an implementation of IUnitOfWork instance, which you could then use to start a transaction and do the usual commit / rollback on. The second is to use the UnitOfWorkScope class:
104 using (var scope = new UnitOfWorkScope())
105 {
106 //Do something here...
107 scope.Commit();
108 }
So the next question that would always follow is…
Q2: So whats a UnitOfWorkScope? How is it different from a UnitOfWork?
The raison d'etre of UnitOfWorkScope is to allow sharing of a single IUnitOfWork, and by extension the underlying ORM context (DataContext, ObjectContext, ISession, etc.) between different components and not have to pass around IUnitOfWork instances.
The UnitOfWorkScope works along the same lines as a TransactionalScope. When you create a UnitOfWorkScope instance, if a compatible ambient UnitOfWorkScope instance already exists, it participates as a child of the ambient scope’s transaction.
While technically you could achieve atomic transactions by wrapping all unit of work operations around a TransactionalScope, but sharing the same underlying IUnitOfWork context between different components would require you to pass around the IUnitOfWork instances. Naturally, the next question that arises is, why would I want to share IUnitOfWork contexts? Wouldn’t it be easier to let each component start UnitOfWork instances and do their operations, and then once everything is done complete the scope?
Consider the following example:
103 public class OrdersService
104 {
105 private IRepository<Order> _ordersRepository;
106 private InventoryService _inventoryService;
107
108 public void CreateOrder(ShoppingCart cart)
109 {
110 using (var scope = new TransactionScope())
111 using (var uow = UnitOfWork.Start())
112 {
113 var order = new Order();
114 //Code to create an order from the shopping cart
115 _ordersRepository.Save(order);
116 _inventoryService.ReserveItems(order);
117 uow.TransactionalFlush();
118 }
119 }
120 }
121
122 public class InventoryService : IInventoryService
123 {
124 private IRepository<OrderReservation> _reservationRepository;
125
126 public void ReserveItems(Order order)
127 {
128 using (var uow = UnitOfWork.Start())
129 {
130 //Reserve stock from order items... OrderReservationEntry has an association to Order
131 var reservationEntry = new OrderReservation(order);
132 _reservationRepository.Save(reservationEntry); //THIS WILL THROW AN EXCEPTION.
133 uow.TransactionalFlush();
134 }
135 }
136 }
137
The example above, the OrdersService basically creates an order, adds it to it’s order repository and then asks the InventoryService to reserve stock for the order. The InventoryService creates an reservation entry which has an association to an Order instance, which is then saved to an OrderReservation repository.
When adding the reservation entry to the repository, an exception will be thrown. Why? Because the reservation service creates an new instance of IUnitOfWork by calling UnitOfWork.Start(), which internally creates an new context (ObjectContext / DataContext / ISession / whatever…) and the moment the OrderReservation instance is saved, the underlying context is going to puke an exception that the Order instance is already part of another context.
So while using a TransactionScope does give transacitonal atomicity, it doesn’t solve the problem of being able to share the same UnitOfWork instance across multiple components.
Getting rid of confusion. One class to rule them all
So while working on 1.1, I decided to nuke UnitOfWork. There’s no reason why there should be two ways to start a unit of work, and UnitOfWorkScope is a simple API to do so. This means that if you are manually starting and flushing unit of works using UnitOfWork.Start(), when moving to 1.1 there will be a lot of broken code. This is a breaking change!.
This decision was not made lightly though. Deciding on introducing a breaking change is a tough one, and I believe at this stage it is required to reduce the confusion around the unit of work implementation in NCommon.
Multi database support
By far the number one request has been to add multi-db support in NCommon. Now technically there was a way you “could” simulate multi database support in 1.0. In 1.0, all IUnitOfWorkFactory implementations define a registration method that would take in a Func<T> to resolve instances of ISessionFactory (for NH) or ObjectContext (for EF) or DataContext (for L2S), and you could add some context specific code in the Func<T> implementation to return different instances based on context. A simple example below:
53 NHUnitOfWorkFactory.SetSessionProvider(() =>
54 {
55 if (DatabaseContext.Current == "OrdersDatabase")
56 return Storage.AppStorage.Application.Get<ISessionFactory>("ordersDB").OpenSession();
57 return Storage.AppStorage.Application.Get<ISessionFactory>("inventioryDB").OpenSession();
58 });
While this approach would work, it’s not ideal. One the features I wanted to have in NCommon is have the underlying database context be automatically resolved without having to specify which database the code is accessing.
Configuring NCommon for multiple database support:
In 1.1, the new configuration class is used to configure the data providers for NCommon. The static SetXXX static methods are not exposed by IUnitOfWorkFactory implementations anymore. Instead you use the ConfigureData method exposed by the Configure class in NCommon:
63 NCommon.Configure.Using(container)
64 .ConfigureData<NHConfiguration>();
The above example uses NHConfiguration to configure the NHibernate data provider for NCommon. Similar configurations for Linq2Sql and EntityFramework exists. The configuration classes provided by each provider expose a single method that allows registering a Func<T> to resolve instances of ISession / DataContext / ObjectContext.
393 NCommon.Configure.Using(container)
394 .ConfigureData<NHConfiguration>(x =>
395 x.WithSessionFactory(() => OrdersDomainFactory)
396 );
You can then register multiple factories / contexts for multiple databases like so:
393 NCommon.Configure.Using(container)
394 .ConfigureData<NHConfiguration>(x =>
395 x.WithSessionFactory(() => OrdersDomainFactory)
396 .WithSessionFactory(() => HRDomainFactory)
397 );
Querying multiple databases:
Once you’ve registered the factories / contexts querying multiple databases is super simple, a concept inspired by NHibernate Burrow.
343 using (var scope = new UnitOfWorkScope())
344 {
345 var savedCustomer = new NHRepository<Customer>()
346 .Where(x => x.CustomerID == customer.CustomerID)
347 .First();
348
349 var savedPerson = new NHRepository<SalesPerson>()
350 .Where(x => x.Id == salesPerson.Id)
351 .First();
352
353 Assert.That(savedCustomer, Is.Not.Null);
354 Assert.That(savedPerson, Is.Not.Null);
355 scope.Commit();
356 }
In the above example, Customer is part of the orders database, while SalesPerson is part of the HR database. The UnitOfWorkScope will automatically resolve the appropriate context that the repository should use. This will work for all three providers; NHibernate, EntityFramework and Linq2Sql.
Transaction Management in 1.1
In 1.0, the UnitOfWorkScope would create instances of IUnitOfWOrk behind the scenes and then call BeginTransaction and CommitTrasaction, or RollbackTransaction, on the instance directly. In the case of multiple databases, an instance of IUnitOfWork instance is created for each context and BeginTransaction and CommitTransaction/RollbackTransaction is called on each instance during the lifecycle of the UnitOfWorkScope instance.
This is what I would call manual transaction management.
In 1.1 manual transaction management is completely removed. In fact if you take a look at the IUnitOfWork interface now, there will be just one method:
27 public interface IUnitOfWork : IDisposable
28 {
29 /// <summary>
30 /// Flushes the changes made in the unit of work to the data store.
31 /// </summary>
32 void Flush();
33 }
The reason of removing transactional management from IUnitOfWork is because there already exists a simple way of managing transactions in .Net, the TransactionScope. Using TransactionScope has been a hard decision actually, since it has the tendency to promote the transaction to a DTC transaction, and even though I loath the DTC, there’s no way around it.
The primary reason is when using multiple databases in a UnitOfWorkScope, when using manual transaction management it doesn’t 100% ensure atomicity. Consider the scenario where, while comitting, the CommitTransaction is called on the first IUnitOfWork instance, which succeeds, and while calling Commit on the second IUnitOfWork instance something bad happens. There’s no way to rollback the first transaction as the underlying IDbTransaction has already comitted.
So the solution would have to be to wrap the entire operation inside a TransactionScope instance. Instead of having an implicit expectation that a TransactionScope is wrapping the entire UnitOfWorkScope operation, the UnitOfWorkScope does the right thing and creates a TransactionScope internally and delegates transaction management to it.
That being said, if an ambient transaction already exists, and it is compatible with the IsolationLevel of the UnitOfWorkScope, the TransactionScope instance it creates will participate in the ambient transaction.
While using TransactionScope simplifies transaction management in NCommon, one thing you should be aware of is that transactions could be enlisted in DTC in certain scnearios. If you using NCommon with a single database, the transaction is enlisted with the LTM and it’s never promoted, unless an existing promoted ambient transaction has already started. If you are using multiple databases, chances are the transaction will be promoted to DTC.
Isolation levels and AutoCompleteScope in 1.1
NCommon 1.0 had constructor overloads for UnitOfWorkScope that would allow you to specify the isolation level that the scope should use. Now, I don’t know about anyone else, but I have never seen an application that uses different isolation levels at different places. So the in 1.1, these overloads are gone.
Additionally, you could provide a UnitOfWorkScopeOptions enumeration to specify if UnitOfWorkScope instances should auto complete while disposing, allowing you to skip having to write the statement scope.Complete everywhere. This again is a global wide configuration in my opinion.
To configure these global settings for UnitOfWorkScope instances, you can use the ConfigureUnitOfWork method exposes by the Configure class:
394 NCommon.Configure.Using(container)
395 .ConfigureData<NHConfiguration>(x =>
396 x.WithSessionFactory(() => OrdersDomainFactory)
397 .WithSessionFactory(() => HRDomainFactory))
398 .ConfigureUnitOfWork<DefaultUnitOfWorkConfiguration>(x =>
399 x.WithDefaultIsolation(IsolationLevel.ReadCommitted)
400 .AutoCompleteScope()
401 );
In the code snippet above, the default isolation level is set to ReadComitted and scopes are set to auto complete.
The only overload now exposed by the UnitOfWorkScope class is the option to not enlist the UnitOfWorkScope instance in an existing ambient transaction/unit of work and to start a new transaction/unit of work.
Final thoughts
The above changes should reduce some of the complexity around setting up and using NCommon. Check out these changes in the 1.1 branch.