LitwareHR on SSDS - Part VI - Unit of Work support

For years now, we have been using transactions in our systems. Years ago, there's been a democratization of the more complex concepts underlying transaction management and we've been quite happy. On the Microsoft side there's been technologies like: MTS, COM+, Enterprise Services, DTC and of course SQL Server; all of them providing great abstractions for single or distributed transaction management.

In its current incarnation, SSDS is not aware of any in-progress client side transaction. That means that if you are writing code that is hosted in COM+ for example, and you are interacting with "transaction aware" resources (e.g.: on-premise databases, MSMQ, etc) and SSDS; then SSDS will be immune and agnostic of any commits or rollbacks. Any successful operation on SSDS will be final, durable. It is up to you to "undo" anything in SSDS in case of an exception and a rollback.

This holds true, even if you only interact with SSDS. There are no "begin transaction"/"commit"/"rollback" semantics in SSDS. Each successful create, delete, update is final and consistency between operations is an application level responsibility.

In LitwareHR we did not have the requirement of interacting with multiple, heterogeneous resources. All our storage is in SSDS, so we needn't support for distributed transactions. However, we needed support for atomicity: that is treating a set of operations as a unit (e.g. during provisioning, where multiple entities are created in LitwareHR's own container: a Tenant entity, permission sets, defaults, styles, etc.)

If something failed, we wanted to be able to undo anything that might have been done in a simple way. In other words we wanted a "Unit of Work" (UoW) for a set of operations against SSDS, without pushing too many details to the application developer.

Once again, the Repository class was extended to support the concept of a UoW. This allows details to be hidden to higher layers of the application. As people might want to implement more or less sophisticated versions of UoW, Repository simply understands the semantics of this interface:

 

public delegate void Operation();

public interface IUoWManager
{
Guid UoWId { get; }
void Begin();
void Commit();
void Rollback();
void RecordOnRollback( Operation Undo, IEntity entity );
void RecordOnCommit( Operation Confirm, IEntity entity );
}

The "UoW-aware" Repository operations now have two versions. For example Insert signatures are:

 public T Insert(T entity, IUoWManager UoWManager );

 public T Insert(T entity); 

In this way, UoWManager is essentially a context that can be passed across multiple operations for final confirmation or deletion.

Here are two tests that demonstrate how to use the new API:

 [TestMethod]
public void Insert2EntitesCommit()
{
    SimpleUoWManager uow = new SimpleUoWManager();

    using (Repository<MockEntity> r = new Repository<MockEntity>(tenantId))
    {
        Guid id1 = Guid.NewGuid();
        MockEntity e1 = new MockEntity(id1);

        Guid id2 = Guid.NewGuid();
        MockEntity e2 = new MockEntity(id2);

        uow.Begin();

           r.Insert(e1, uow);
           r.Insert(e2, uow);

        uow.Commit();

        Assert.IsNotNull(r.GetById(id1));
        Assert.IsNotNull(r.GetById(id2));
    }
}

[TestMethod]
public void Insert2EntitesRollback()
{
    SimpleUoWManager uow = new SimpleUoWManager();

    using (Repository<MockEntity> r = new Repository<MockEntity>(tenantId))
    {
        Guid id1 = Guid.NewGuid();
        MockEntity e1 = new MockEntity(id1);

        Guid id2 = Guid.NewGuid();
        MockEntity e2 = new MockEntity(id2);

        uow.Begin();

           r.Insert(e1, uow);
           r.Insert(e2, uow);

        uow.Rollback();

        Assert.IsNull(r.GetById(id1));
        Assert.IsNull(r.GetById(id2));
    }
}

SimpleUoWManager is well...simple; so it doesn't take into account things like machine shutdown, etc. If the process this code is running on dies unexpectedly in the middle of a UoW, then data stored in SSDS might be inconsistent from an application perspective.

In this implementation SimpleUoWManager simply stores a collection of operations (callbacks) to be called when the confirmation is received (Commit is called) or a list of "undo" operations to call if there's a rollback. The "undo" of an insert is a delete, the undo of an update is an update with a previous version, etc.

Repository is responsible for defining what "undo" or "confirm" really mean. For example, Insert with a UoWManager looks like this:

 public T Insert(T entity, IUoWManager mgr)
{
    mgr.RecordOnRollback( () => Delete( entity.Id ), entity );
    mgr.RecordOnCommit(() => ConfirmInsert(entity), entity);

    InsertTentative( entity, mgr.UoWId );

    return entity;
} 

and InsertTentative simply adds a UOW_ID property to the entity to signal that it is part of a UOW:

 private void InsertTentative(T entity, Guid uow)
{
    entity.Fields.Add( "UOW_ID", uow.ToString());
    Insert(entity);
}

ConfirmInsert removes that property.

Obviously this doesn't work with Enterprise Services aware resources (like SQL Server, MSMQ, etc). For that, you should look at implementing a Resource Manager that works with COM+.