23 August 2006

Attribute based transaction management and NHibernate

In my previous post about NHibernate session management I presented a session per request strategy for session management in ASP.NET application. I will extend this example to show you how to declaratively manage transactions via attributes. Take a look at the GenericDAO.Save method:

public void Save(T entity)
{
    bool manageTransaction = ! orm.isActiveTransaction;
    if (manageTransaction)
    {
        orm.BeginTransaction(IsolationLevel.ReadCommitted);
    }
    try
    {
        orm.session.SaveOrUpdate(entity);
 
        if (manageTransaction)
        {
            orm.CommitTransaction();
        }
    }catch(Exception e)
    {
        if (manageTransaction)
        {
            orm.RollbackTransaction();
        }
 
        throw e;
    }            
}

Almost 90% of the code is for transaction management. It will be nice if we can write something like:

[Transaction(IsolationLevel.ReadCommitted)]
public void Save(T entity)
{
    mOrm.session.SaveOrUpdate(entity);
    mOrm.session.Flush();
}

To achieve this, I will use Spring.NET to intercept method call of the DAO. Before method marked with TransactionAttribute is executed a transaction will be started and then committed after method is executed. Although .NET provides solution for intercepting method calls via ContextBoundObject, I will use Spring.NET because this way my DAO object must implement just one interface instead of inheriting from ContextBoundObject.
Lets begin with the implementation. First I will declare the attribute used to mark transaction sensitive methods:

[AttributeUsage(AttributeTargets.Method)]
public class TransactionAttribute : Attribute
{
    private IsolationLevel mIsolationLevel;
 
    public TransactionAttribute()
    {
        mIsolationLevel = IsolationLevel.ReadCommitted;
    }
 
    public TransactionAttribute(IsolationLevel aIsolation)
    {
        mIsolationLevel = aIsolation;
    }
 
    public IsolationLevel isolationLevel
    {
        get { return mIsolationLevel; }
        set { mIsolationLevel = value; }
    }
 
}

Now I will decrare the class responsible for starting and committing transactions. There are a lot of specific issues about method interception and Spring.NET so if you are not familiar with them read the documentation about Spring.NET. The class looks like:

public class TransactionAroundAdvice : IMethodInterceptor
{
    OrmManager mOrm;
 
    public TransactionAroundAdvice()
    {
        mOrm = OrmManagerFactory.GetInstance();
    }
 
    private TransactionAttribute GetTransactionAttribute(MethodInfo aMethod)
    {
        object[] attributes = aMethod.GetCustomAttributes(
            typeof(TransactionAttribute), true);
 
        if (attributes.Length != 1)
        {
            //TODO: Throw more meaningfull exception and message
            throw new Exception("Transaction Attribute is missing");
        }
        return (TransactionAttribute)attributes[0];
    }
 
    public object Invoke(IMethodInvocation invocation)
    {
        bool manageTransaction = ! mOrm.isActiveTransaction;
        if (manageTransaction)
        {
            TransactionAttribute transactionAttribute =
                GetTransactionAttribute(invocation.Method);
            mOrm.BeginTransaction(transactionAttribute.isolationLevel);
        }
 
        //TODO: Find way to compare isolation level from TransactionAttribute and
        //current active transaction if manageTransaction == false
 
        object returnValue;
        try
        {
            returnValue = invocation.Proceed();
 
            if (manageTransaction)
            {
                mOrm.CommitTransaction();
            }
        }catch(Exception e)
        {
            if (manageTransaction)
            {
                mOrm.RollbackTransaction();
            }
 
            if ((e is TargetInvocationException) && (e.InnerException != null))
            {
                //Because when AOP is enables, method is invoked via reflection
                //TargetInvocationException is thrown. This exception is most likely
                //to confuse application developer, so it is removed
                throw e.InnerException;
            }
            else
            {
                throw e;
            }
        }            
 
        return returnValue;
    }
}

When a method marked with TransactionAttribute is intercepted then Invoke method from TransactionAroundAdvice class is called. Now lets see how our DAO class looks like when transaction management code is removed:

public class AttributeEnabledDAO<T>: IDAO<T>
{
    public AttributeEnabledDAO()
    {
        mOrm = OrmManagerFactory.GetInstance();
    }
 
    private OrmManager mOrm;
 
    #region IDAO Members
 
    public T Load(object id)
    {
        return (T) mOrm.session.Load(typeof(T), id);
    }
 
    [Transaction(IsolationLevel.ReadCommitted)]
    public void Save(T entity)
    {
        mOrm.session.SaveOrUpdate(entity);
        mOrm.session.Flush();
    }
 
    #endregion
}

Much more simple. Example of using AttributeEnabledDAO is given bellow:

ProxyFactory articleDAOFactory = new ProxyFactory(
    new AttributeEnabledDAO<Article>());
 
articleDAOFactory.AddAdvisor(new DefaultPointcutAdvisor(
    new AttributeMatchMethodPointcut(typeof(TransactionAttribute)),
    new TransactionAroundAdvice()));
 
IDAO<Article> articleDao;
lock (synchObject)
{
    articleDao = (IDAO<Article>) articleDAOFactory.GetProxy();
}
 
Article article = articleDao.Load(1);
article.Name = "Paracetamol";
article.SalePrice = rnd.NextDouble();
articleDao.Save(article);

No comments: