25 July 2006

NHibernate Session Management

I'm big fan of NHibernate since early beta. Unfortunately this tool is not very popular among .NET developers, so I'm planning to blog a lot about this framework. For those of you who don't know, NHibernate is ORM (Object-Relational-Mapping) tool. Basically ORM tools are used to "persist" C# objects into RDBMS system. More detail explanation of ORM tools is available here.
NHibernate handles loading/saving objects to RDBMS via it's own implementation of session object. Until now, I'm familiar with 3 ways for handling NHibernate sessions:
  • create session per each database operation - session is created before and destroyed after each operation with database. This is very ineffective way, because some features like lazy loading are unavailable. Also including several database operations in transaction is impossible.
  • create session per each request - session is created before and destroyed after each HTTP request. This is my favorite strategy, because it is effective and very easy for implementation. I'll give example for possible implementation in this post.
  • create session per each application transaction - session is created before and destroyed after each application transaction. Example for application transaction is adding invoice with invoice items.

So, lets begin with the implementation of session per request strategy. If you are reading this post you are most likely a developer and probably as most developers prefer to read source code instead of boring documentation. So I'll explain all of the classes in brief and will post full source code. First I'll declare a base abstract class for all of the session strategies.



public abstract class OrmManager
{
    public OrmManager()
    {
    }
 
    static NHibernate.Cfg.Configuration mConfiguration = null;
    static volatile ISessionFactory mSessionFactory = null;
    static object mFactorySyncObject = new object();
 
    public abstract ISession session
    {
        get;
    }
 
    public IDbConnection sessionConnection
    {
        get { return session.Connection; }
    }
 
    public ISession NewSession()
    {
        ISessionFactory sessionFactory = GetSessionFactory();
 
        return sessionFactory.OpenSession();
    }
 
    public abstract bool isActiveSession
    {
        get;
    }
 
    public abstract void closeSession();
 
    private const string HBM_ASSEMBLY_NAME_SECTION = "HbmAssemblyName";
 
    private ISessionFactory GetSessionFactory()
    {
        if (mSessionFactory == null)
        {
            lock (mFactorySyncObject)
            {
                if (mSessionFactory == null)
                {
                    string hbmAssemblyName =
                        ConfigurationManager.AppSettings[HBM_ASSEMBLY_NAME_SECTION];
 
                    mConfiguration = new NHibernate.Cfg.Configuration();
                    mConfiguration.AddAssembly(hbmAssemblyName);
 
                    mSessionFactory = mConfiguration.BuildSessionFactory();
                }
            }
        }
 
        return mSessionFactory;
    }
 
    public void BeginTransaction(IsolationLevel isolation)
    {
        session.BeginTransaction(isolation);
    }
 
    public void CommitTransaction()
    {
        session.Transaction.Commit();
    }
 
    public void RollbackTransaction()
    {
        session.Transaction.Rollback();
    }
 
    public bool isActiveTransaction
    {
        get
        {
            return ((session.Transaction != null) &&
                (!session.Transaction.WasCommitted) &&
                (!session.Transaction.WasRolledBack));
        }
    }
}

Next, I'll implement the class responsible for session per request management. The following class implements IHttpModule so, we can handle end of the request and close any open sessions.


public class PerRequestWebManager : OrmManager, IHttpModule
{
    public PerRequestWebManager()
    {
    }
 
    private const string NHIBERNATE_SESSION_KEY = "NHIBERNATE_SESSION";
 
    public override ISession session
    {
        get
        {
            ISession resultSession;
 
            if (HttpContext.Current.Items.Contains(NHIBERNATE_SESSION_KEY))
            {
                resultSession =
                    (ISession)HttpContext.Current.Items[NHIBERNATE_SESSION_KEY];
            }
            else
            {
                resultSession = NewSession();
                HttpContext.Current.Items[NHIBERNATE_SESSION_KEY] = resultSession;
            }
 
            return resultSession;
        }
    }
 
    public override bool isActiveSession
    {
        get { return HttpContext.Current.Items.Contains(NHIBERNATE_SESSION_KEY); }
    }
 
    public override void closeSession()
    {
        if (isActiveSession == true)
        {
            if (isActiveTransaction == true)
            {
                RollbackTransaction();
            }
 
            session.Close();
            HttpContext.Current.Items.Remove(NHIBERNATE_SESSION_KEY);
        }
    }
 
    #region IHttpModule
 
    public void Init(HttpApplication context)
    {
        context.EndRequest += new EventHandler(OnEndRequest);
    }
 
    public void Dispose()
    {
        //Intentionaly not implemented
    }
 
    public void OnEndRequest(Object sender, EventArgs e)
    {
        closeSession();
    }
 
    #endregion
}

Now I'll make one generic DAO class with the help of previos PerRequestWebManager.


public class GenericDAO<T>
{
    public GenericDAO()
    {
        orm = OrmManagerFactory.GetInstance();
    }
 
    private OrmManager orm;
 
    public T Load(object id)
    {
        return (T) orm.session.Load(typeof(T), id);
    }
 
    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;
        }            
    }
}

public class OrmManagerFactory
{
    private OrmManagerFactory()
    {
    }
 
    private const string ORM_MANAGER_TYPE_SECTION = "OrmManager";
 
    public static OrmManager GetInstance()
    {
        string ormType = ConfigurationManager.AppSettings[ORM_MANAGER_TYPE_SECTION];
        if (ormType == typeof(PerRequestWebManager).Name)
        {
            return new PerRequestWebManager();
        }
        else
        {
            throw new Exception("Unknown ORM manager type");
        }
    }
}

And here's how all of the above can be used in ASP.NET application:


GenericDAO<Article> articleDao = new GenericDAO<Article>();
 
Article article = articleDao.Load(1);
article.Name = "Paracetamol";
article.SalePrice = rnd.NextDouble();
articleDao.Save(article);

If you are interested I can mail you full source of this post. Enjoy!

4 comments:

Anonymous said...

How would you manage ISessions if you were to create a windows application instead of a web-application?

I have been using session per request so far and it worked really fine until I discovered that it breaks lazy loading (cause I've already closed the session).

Stephan Zahariev said...

If I'm developing Windows based application I will store session to a singleton object. Inside the singleton object session will be referenced via static ISession member. Also I will have to manually clear session cache (for example when user completes a business transaction).
If you want to use lazy loading, you must implement session per application transaction. Instead of closing session, just disconnect connection and store the session in the ASP.NET session. When second request arrives, just renew connection to the server. In this scenario, you must manually close the session.

Anonymous said...

Ok, from what i have read so far, your post has made my understand this pattern. I will appreciate if u can send me the source at adrian@saagoo.com. Thanks.

PS: From what I know lazy loading is a default strategy in Nhibernate 1.2 b3. Does this pattern (session per request) breaks lazy loading in Nhibernate 1.2beta3?

Stephan Zahariev said...

This pattern supports lazy loading during the lifetime of the session object. Session object is closed at the end of each request, so lazy loading is supported within the request only. If you want to spread lazy loading among several request you should close only DB connection at the end of request, as a mentioned in the previous comment.