Making ASP.NET MVC Actions be Transactional By Default
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);
}
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);
}
Situation Three – We specify HandleTransactionManually and we get no transaction
[HandleTransactionManually]
public ActionResult Index()
{
var data = //get data
return View(data);
}
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!