Actually i try to found the best way to manage Transaction with Nhibernate with Repository pattern in MVC 5 context
You can found my sample project here: https://github.com/Nono31/Pixel.Sample
My repository called by manager
And my manager was called by controller
Actually all works fine, but when I start NHProfiler, I have a warning "Use of implicit transactions is discouraged"
(http://www.hibernatingrhinos.com/products/nhprof/learn/alert/donotuseimplicittransactions)
My question is how to avoid implicit Transaction in my context ?
In wich layer manage the transaction ?
If I manage my transaction on Repository layer, the lazy loading entity are called outside the transaction.
I've seen one solution with ActionFilterAttribute but there are any other solution ?
public class UnitOfWorkAction : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
if (!context.IsChildAction)
{
var session = DependencyResolver.Current.GetService<ISession>();
session.BeginTransaction();
}
}
public override void OnResultExecuted(ResultExecutedContext context)
{
base.OnResultExecuted(context);
if (context.IsChildAction) return;
using (var session = DependencyResolver.Current.GetService<ISession>())
{
if (session.Transaction != null && session.Transaction.IsActive)
using (var transaction = session.Transaction)
{
try
{
var thereWereNoExceptions = context.Exception == null || context.ExceptionHandled;
if (context.Controller.ViewData.ModelState.IsValid && thereWereNoExceptions)
transaction.Commit();
else
transaction.Rollback();
}
catch
{
transaction.Rollback();
throw;
}
finally
{
session.Close();
}
}
}
}
}
You should transact your entire unit of work. Unit of work should cover the work you do to fetch your view models.
Doing so will avoid lazy-loading to occur out of a transaction.
It will also allow you to rollback the complete unit of work in case of error.
And it has a performance benefit: one of the reasons for NHProfiler warning is the cost of opening a transaction for each data access. (There is other ones, such as the second level cache requiring explicit transaction, otherwise it gets disabled on updates.)
You may use the UnitOfWorkAction
you have found.
Personally, I find it too 'broad'. It includes in transaction even the result execution. This allows using lazy-loading in views. I consider we should not use entities as view models, and triggering db access from views is even worse in my mind. The one I use ends the transaction in OnActionExecuted
.
Moreover, its error handling looks to me a bit specific. Roll-backing on invalid model state may not make sens: no action is supposed to try saving invalid data in DB. Not roll-backing on handled exceptions is strange for the less: if the MVC pipeline have seen an exception, this means something went wrong in the action or while executing the result, but some other filters has handled the exception. Usually, just an error filter displaying an error page, isn't it? Then such a logic causes to commit a failed action...
Here is the pattern of the one I use:
public class DefaultTransactionAttribute : ActionFilterAttribute
{
private static readonly ILog Logger =
LogManager.GetLogger(typeof(DefaultTransactionAttribute));
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// IUnitOfWork is some kind of custom ISession encapsulation.
// I am working in a context in which we may change the ORM, so
// I am hiding it.
var uow = DependencyResolver.Current.GetService<IUnitOfWork>();
uow.BeginTransaction();
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
var uow = DependencyResolver.Current.GetService<IUnitOfWork>();
if (!uow.HasActiveTransaction())
{
// Log rather than raise an exception, for avoiding hiding
// another failure.
Logger.Warn("End of action without a running transaction. " +
"Check how this can occur and try avoid this.");
return;
}
if (filterContext.Exception == null)
{
uow.Commit();
}
else
{
try
{
uow.Rollback();
}
catch(Exception ex)
{
// Do not let this new exception hide the original one.
Logger.Warn("Rollback failure on action failure. (If the" +
"transaction has been roll-backed on db side, this is" +
"expected.)", ex);
}
}
}
}
For a step by step explanation of a minimal MVC pattern, see this great blog series from Ayende:
- Refactoring, baseline
- Refactoring, global state
- Refactoring, session scope
- Refactoring, broken
- Refactoring, view model
- Refactoring, globals
- Refactoring, transactions
Since my IUnitOfWork
has some special semantics which help me in the MVC pattern I use with NHibernate, here it is:
// This contract is not thread safe and must not be shared between threads.
public interface IUnitOfWork
{
/// <summary>
/// Save changes. Generally unneeded: if a transaction is ongoing,
/// its commit does it too.
/// </summary>
void SaveChanges();
void CancelChanges();
bool HasActiveTransaction();
void BeginTransaction();
/// <summary>
/// Saves changes and commit current transaction.
/// </summary>
void Commit();
void Rollback();
/// <summary>
/// Encapsulate some processing in a transaction, committing it if
/// no exception was sent back, roll-backing it otherwise.
/// The <paramref name="action"/> is allowed to rollback the transaction
/// itself for cancelation purposes. (Commit supported too.)
/// Nested calls not supported (InvalidOperationException). If the
/// session was having an ongoing transaction launched through direct
/// call to <c>>BeginTransaction</c>, it is committed, and a new
/// transaction will be opened at the end of the processing.
/// </summary>
/// <param name="action">The action to process.</param>
void ProcessInTransaction(Action action);
/// <summary>
/// Encapsulate some processing in a transaction, committing it if
/// no exception was sent back, roll-backing it otherwise.
/// The <paramref name="function"/> is allowed to rollback the transaction
/// itself for cancellation purposes. (Commit supported too.)
/// Nested calls not supported (InvalidOperationException). If the
/// session was having an ongoing transaction launched through direct
/// call to <c>>BeginTransaction</c>, it is committed, and a new
/// transaction will be opened at the end of the processing.
/// </summary>
/// <param name="function">The function to process.</param>
/// <typeparam name="T">Return type of
/// <paramref name="function" />.</typeparam>
/// <returns>The return value of the function.</returns>
T ProcessInTransaction<T>(Func<T> function);
}
public class UnitOfWork : IUnitOfWork
{
private static readonly ILog Logger =
LogManager.GetLogger(typeof(UnitOfWork));
private ISession Session;
public UnitOfWork(ISession session)
{
Session = session;
}
public void SaveChanges()
{
Session.Flush();
}
public void CancelChanges()
{
Session.Clear();
}
public bool HasActiveTransaction()
{
return Session.Transaction.IsActive;
}
public void BeginTransaction()
{
Session.BeginTransaction();
}
public void Commit()
{
Session.Transaction.Commit();
}
public void Rollback()
{
Session.Transaction.Rollback();
}
public void ProcessInTransaction(Action action)
{
if (action == null)
throw new ArgumentNullException("action");
ProcessInTransaction<object>(() =>
{
action();
return null;
});
}
private bool _processing = false;
public T ProcessInTransaction<T>(Func<T> function)
{
if (function == null)
throw new ArgumentNullException("function");
if (_processing)
throw new InvalidOperationException(
"A transactional process is already ongoing");
// Handling default transaction.
var wasHavingActiveTransaction = Session.Transaction.IsActive;
if (wasHavingActiveTransaction)
Commit();
BeginTransaction();
T result;
_processing = true;
try
{
result = function();
}
catch
{
try
{
if(Session.Transaction.IsActive)
Rollback();
}
catch (Exception ex)
{
// Do not let this new exception hide the original one.
Logger.Error("An additional error occurred while " +
"attempting to rollback a transaction after a failed " +
"processing.", ex);
}
// Let original exception flow untouched.
throw;
}
finally
{
_processing = false;
}
if (Session.Transaction.IsActive)
Commit();
if (wasHavingActiveTransaction)
BeginTransaction();
return result;
}
}
I keep the current session as an property in global.asax and open it on BeginRequest.
public static ISession CurrentSession
{
get { return (ISession)HttpContext.Current.Items[sessionkey]; }
set { HttpContext.Current.Items[sessionkey] = value; }
}
protected void Application_BeginRequest()
{
CurrentSession = SessionFactory.OpenSession();
}
protected void Application_EndRequest()
{
if (CurrentSession != null)
CurrentSession.Dispose();
}
Then I have a Transaction attribute that you can tag each controller action with.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class TransactionAttribute : ActionFilterAttribute
{
private ITransaction Transaction { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Transaction = MvcApplication.CurrentSession.BeginTransaction(IsolationLevel.ReadCommitted);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (Transaction.IsActive)
{
if (filterContext.Exception == null)
{
Transaction.Commit();
}
else
{
Transaction.Rollback();
}
}
}
}
And in my repository I have a transaction method that will start a transaction if there isn't currently one active.
protected virtual TResult Transact<TResult>(Func<TResult> func)
{
if (_session.Transaction.IsActive)
return func.Invoke();
TResult result;
using (var tx = _session.BeginTransaction(IsolationLevel.ReadCommitted))
{
result = func.Invoke();
tx.Commit();
}
return result;
}