views:

695

answers:

3

What is the best approach to managing NHibernate transaction using Autofac within web application?

My approach to session is

builder.Register(c => c.Resolve<ISessionFactory>().OpenSession())
       .ContainerScoped();

For ITransaction, I have found an example on Google Code, but it relies on HttpContext.Current.Error when deciding whether to rollback.

Is there a better solution? And what scope NHibernate transaction should have?

A: 

I usually manage the transaction myself..

public ActionResult Edit(Question q){
try {
 using (var t = repo.BeginTransaction()){
  repo.Save(q);
  t.Commit();
  return View();
 }
 catch (Exception e){
  ...
 }
}
Carl Hörberg
It's the simplest solution, but it is a bit tedious and adds indent to the whole method.
Andrey Shchekin
sure, 2 lines and one indent, but some day you realize that you have to do two transactions in the same request or something like that..
Carl Hörberg
Most people don't like to press ctrl+c/ctrl+v so many times
Paco
+3  A: 

I posted this a while ago:

http://groups.google.com/group/autofac/browse%5Fthread/thread/f10badba5fe0d546/e64f2e757df94e61?lnk=gst&amp;q=transaction#e64f2e757df94e61

Modified, so that the interceptor has logging capability and [Transaction] attribute can also be used on a class.

[global::System.AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class TransactionAttribute : Attribute
{
}


public class ServicesInterceptor : Castle.Core.Interceptor.IInterceptor
{
    private readonly ISession db;
    private ITransaction transaction = null;

    public ServicesInterceptor(ISession db)
    {
        this.db = db;
    }

    public void Intercept(IInvocation invocation)
    {
        ILog log = LogManager.GetLogger(string.Format("{0}.{1}", invocation.Method.DeclaringType.FullName, invocation.Method.Name));

        bool isTransactional = IsTransactional(invocation.Method);
        bool iAmTheFirst = false;

        if (transaction == null && isTransactional)
        {
            transaction = db.BeginTransaction();
            iAmTheFirst = true;
        }

        try
        {
            invocation.Proceed();

            if (iAmTheFirst)
            {
                iAmTheFirst = false;

                transaction.Commit();
                transaction = null;
            }
        }
        catch (Exception ex)
        {
            if (iAmTheFirst)
            {
                iAmTheFirst = false;

                transaction.Rollback();
                db.Clear();
                transaction = null;
            }

   log.Error(ex);
   throw ex;
        }
    }

    private bool IsTransactional(MethodInfo mi)
    {
        var atrClass = mi.DeclaringType.GetCustomAttributes(false);

        foreach (var a in atrClass)
            if (a is TransactionAttribute)
                return true;

        var atrMethod = mi.GetCustomAttributes(false);

        foreach (var a in atrMethod)
            if (a is TransactionAttribute)
                return true;

        return false;
    }
}
dmonlord
I like the solution. However, another question is to what methods should I apply [Transaction]? According to this http://nhprof.com/Learn/Alert?name=DoNotUseImplicitTransactions it seems I should always have a transaction if I have a session, however this solution requires me to manually add [Transaction] everywhere the session is used.
Andrey Shchekin
Check the modified version. You'll probably know how to modify it from there.You can also skip the [Transaction] attribute altogether and always start the transaction. It's your choice. I needed the attribute, because sometimes I needed to do 2 things in different transactions.I did it like this://No transaction attributepublic virtual void CallThis(){ Trans1(); //commits Trans2(); //commits}[Transaction]public virtual void Trans1() { }[Transaction]public virtual void Trans2() { }
dmonlord
Could you explain the use case for two transactions? This is important since if I may also encounter a similar use case, I will look at the problem differently.
Andrey Shchekin
When a user tries to login with a certificate I have to do some selects and some updates in the database (that is the first transaction). If for some reason the user can't login (blocked account, error in network, whetever...) I have to do a rollback of everything I have done and update (in new transaction) a "Failed login count" up by one (and after it reaches 5 the account is blocked).
dmonlord
I think I understand the context now. I can start and commit a transaction automatically, but a good solution needs to allow manual rollbacks and manual restarts. I'll think about that a bit more, but right now I accept your answer. Thanks.
Andrey Shchekin
Nitpick. its just a simple throw and not throw ex;
Cherian
+2  A: 

When I use autofac I use the same container scoped method but instead of passing the same session to my Repository/DAO objects I pass an UnitOfWork that is container scoped. The Unit of work has this in the constructor.

    private readonly ISession _session;
    private ITransaction _transaction;

    public UnitOfWork(ISession session)
    {
        _session = session;
        _transaction = session.BeginTransaction();
    }

And the dispose is:

    public void Dispose()
    {
        try
        {
            if (_transaction != null &&
                            !_transaction.WasCommitted &&
                            !_transaction.WasRolledBack)
                _transaction.Commit();
            _transaction = null;
        }
        catch (Exception)
        {
            Rollback();
            throw;
        }

    }

I am (ab)using the deterministic disposal stuff in autofac in order to manage this, and well I sort of like it.

The other thing is that I am basically only targeting an ASPNet environment and made a conscious decision that a transaction is tied to a web request. So a transaction per web request pattern.

Because of that I can do this error handling code in an IHttpModule:

    void context_Error(object sender, System.EventArgs e)
    {
        _containerProvider.RequestContainer.Resolve<IUnitOfWork>().Rollback();
    }

I haven't taken a look at NHibernate.Burrow too closely but I'm sure there is something there that does most of this.

Min
Your idea is very similar to what Autofac did, and I like it a lot. Unfortunately, I can not accept two answers, so I'll choose the one that was first (also dmonlord has much less reputation).
Andrey Shchekin