Making ASP.NET MVC Actions be Transactional By Default

In an earlier post I talked about writing a Transaction attribute for MVC using NHibernate (though it isn’t really NHibernate specific).  The basic idea is that when an action marked with [Transaction] is executing (OnActionExecuting) you begin a transaction, and some time later (I use OnActionExecuted, but Kazi Manzur Rashid makes a good argument for OnResultExecuted) you commit that transaction if there was no error, or rollback if there was an error.
 
With that in mind, the implementation details aren’t really important because my motivation in this post is to make every action transactional by default.  Most of the actions on any real site will require retrieving, updating, saving or removing data so as a best practice I would like the user to have to specify if they choose not to use a transaction (maybe for a static about page).
 

What I’d Like to Accomplish

Given any action method (we’ll use Index), if there is no attribute it should execute in a Transaction:

public ActionResult Index()
{
    var data = //get data
    return View(data);
}

Now if we explicitly use a [Transaction] attribute it should still execute in a Transaction:

[Transaction]
public ActionResult Index()
{
    var data = //get data
    return View(data);
}

However, we can choose to use a self-defined [HandleTransactionManually] attribute which will Not Use a Transaction:

[HandleTransactionManually]
public ActionResult Index()
{
    var data = //get data
    return View(data);
}

 

The UseTransactionByDefaultAttribute and the SuperController

To enforce “global” rules like this it is necessary for all controllers to inherit from a custom controller I call the “SuperController”.  This is a class I was using anyway for helper/common methods and I assume a lot of other people don’t inherit directly from the Controller class as well.  The SuperController just inherits from System.Web.Mvc.Controller for the purposes of this post.

Now I am going to start by making a class attribute for the SuperController (or any controller really) which will cause us to use transactions by default.  First I’ll show the implementation and then explain it:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class UseTransactionsByDefaultAttribute : ActionFilterAttribute
{
    private IDbContext _dbContext;
    private bool _delegateTransactionSupport;
 
    public IDbContext DbContext
    {
        get
        {
            if (_dbContext == null) _dbContext = SmartServiceLocator<IDbContext>.GetService();
 
            return _dbContext;
        }
    }
 
 
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        _delegateTransactionSupport = ShouldDelegateTransactionSupport(filterContext);
 
        if (_delegateTransactionSupport) return;
 
        DbContext.BeginTransaction();
    }
 
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (_delegateTransactionSupport) return;
 
        if (DbContext.IsActive)
        {
            if (filterContext.Exception == null)
            {
                DbContext.CommitTransaction();
            }
            else
            {
                DbContext.RollbackTransaction();
            }
        }
    }
 
    private static bool ShouldDelegateTransactionSupport(ActionExecutingContext context)
    {
        var attrs = context.ActionDescriptor.GetCustomAttributes(typeof (TransactionalActionBaseAttribute), false);
 
        return attrs.Length > 0;
    }
}

Explanation – So this is an action filter attribute of course, and it works only on classes.  When the action executes (OnActionExecuting) I check to see if I should “delegate” the transaction support, and if so I’ll return and do nothing else.  If I am not delegating transaction support, I’ll begin the transaction.  The OnActionExecuted method does something similar and if we are not delegating it takes care of committing or doing the rollback depending on need.

The ShouldDelegateTransactionSupport is pretty interesting in that it check the custom attributes on the current action (using ActionDescriptor.GetCustomAttributes()) and if it sees any attributes that inherit from my custom TransactionalActionBaseAttribute class then it return true, meaning that we should delegate transaction support to the attributes.

This base class is pretty simple:

public class TransactionalActionBaseAttribute : ActionFilterAttribute
{
    
}

 

Handling Transactions Manually if Needed

If you want to override the default transaction then we need another attribute which will provide this support for us.  Since we have seen that inheriting from TransactionalActionBaseAttribute means we are delegated responsibility for the transaction, we can just ask for responsibility and then do nothing with it, as below

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class HandleTransactionManuallyAttribute : TransactionalActionBaseAttribute
{
}

 

Results – Show and Tell

Let’s see how this works for us.

Situation One – We don’t specify any transaction attribute and we get a transaction

public ActionResult Index()
{
    var data = //get data
    return View(data);
}

image

 

 

 

 

Situation Two – We specify a transaction attribute and we get the exact same result

[Transaction]
public ActionResult Index()
{
    var data = //get data
    return View(data);
}

image

 

 

 

 

Situation Three – We specify HandleTransactionManually and we get no transaction

[HandleTransactionManually]
public ActionResult Index()
{
    var data = //get data
    return View(data);
}

image

 

I’m a big fan of setting best practice defaults and default transactions are a good way to help get devs to use transactions as much as possible (which for many reasons is a good thing).

Enjoy!

13 Comments

Comments have been disabled for this content.