2
\$\begingroup\$

This code is part of a quite huge repository using NHibernate in a wrong way: no units of work, but microsessions for every object loading and saving (ignorant of Persistence Ignorance, coded like naive SQL or file access).

The TransactionalSessionContainer has been existing for a while now, but, if used, only in a few methods where it was submitted as a parameter (not in sample code).

Recently, for the first time, an NHibernate session context (CallSessionContext, bound to thread + factory) was used to make the session from the TransactionalSessionContainer avaliable without parameters, and to enforce usage of the transaction for all DB operations in thread, while it is open. NHibSessionManager was added as in code, to use a context session if available, or to create and later dispose a microsession (can't refactor it all now).

This resulted in the problem of faulty dipsosal in the TransactionalSessionContainer class, esp. when BeginTransaction() in the constructor failed (due to network problems). A broken session would then remain bound to the context, preventing both microsessions and new context sessions from being created on the context, completely and permanently blocking DB access in that thread (resulting in "Session is closed!" and "ObjectDisposedException").

So, it seems like I have to treat the context binding like an unmanaged resource, fixing not only the constructor (unbind in case of error), but implementing the full MS IDisposable pattern, to make sure the session is unbound anyway if somebody forgets to Dispose().

Is this so far correct (and thus a right step on the long way to a correctly used NHibernate)?

The repository, with old and new access method:

public class Repository
{
    private ISessionFactory _sessionFactory; // build somewhere

    // ...

    public IList<MyItem> GetMyItemsByCreationDate(DateTime from, DateTime to)
    {
        using (var sessMan = new NHibSessionManager(_sessionFactory))
        {
            // originally, a directly created microsession
            var qo = sessMan.Session.QueryOver<MyItem>();
            // add datetime range criteria here
            return qo.List();
        }
    }

    public void Save<T>(T objectToSave)
    {
        using (var sessMan = new NHibSessionManager(_sessionFactory))
        {
            using (ITransaction tran = CurrentSessionContext.HasBind(_sessionFactory) 
                ? null : sessMan.Session.BeginTransaction())
            {
                sessMan.Session.SaveOrUpdate(objectToSave);
                if (tran != null) 
                { 
                    tran.Commit(); // old single object save style (wrong)
                }
            }
        }
    }

    public void DoSomethingWithMyItemsWrong(DateTime from, DateTime to)
    {
        // wrong: microsessions/single object saves
        var myItems = GetMyItemsByCreationDate(from, to);
        foreach (var item in myItems)
        {
            // do something...
            Save(item);
        }
    }

    public void DoSomethingWithMyItemsRight(DateTime from, DateTime to)
    {
        // correct UnitOfWork / short term transaction usage
        using(var tranSessMan = new TransactionalSessionContainer(_sessionFactory))
        {
            var myItems = GetMyItemsByCreationDate(from, to);
            foreach (var item in myItems)
            {
                // do something...
            }
            tranSessMan.Commit();
        }
    }
}

The session manager (for using blocks):

/// <summary>
/// Decide whether to pick context session or create individual
/// </summary>
internal class NHibSessionManager : IDisposable
{
    private ISessionFactory _sessionFactory; 

    public ISession Session { get; private set; }

    public NHibSessionManager(ISessionFactory sessionFactory)
    {
        _sessionFactory = sessionFactory;
         Session = CurrentSessionContext.HasBind(_sessionFactory)
            ? _sessionFactory.GetCurrentSession() // Unit of Work context session
            : _sessionFactory.OpenSession(); // classic (wrong) microsession
         Session.FlushMode = FlushMode.Commit;
    }

    public void Dispose()
    {
        Dispose(true); // not really necessary here, but formally demanded
        GC.SuppressFinalize(this);
    }

    public void Dispose(bool isDisposing)
    {
        if (Session != null && isDisposing)
        {
            Session.Dispose(); // No unbind here
            Session = null;
        }
    }
}

The transactional Unit of Work:

public class TransactionalSessionContainer : IDisposable
{
    private NHibSessionManager _sessionManager;

    public ISession Session { get { return _sessionManager.Session; } }

    private ITransaction _transaction; 

    public TransactionalSessionContainer(ISessionFactory sessionFactory)
    {
        _sessionManager = new NHibSessionManager(sessionFactory);
        CurrentSessionContext.Bind(Session);
        try
        {
            _transaction = Session.BeginTransaction(); // dangerous, toxic context session if failing and not unbound 
        }
        catch
        {
            CurrentSessionContext.Unbind(Session.SessionFactory);
            _sessionManager.Dispose();
            throw;
        }
    }

    ~TransactionalSessionContainer()
    {
        Dispose(false); // GC finalization MUST unbind session from context!
    }


    public void Commit()
    {
        _transaction.Commit();
    }

    public void Rollback() 
    {
        _transaction.Rollback();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public void Dispose(bool isDisposing)
    {
        if (_sessionManager != null)
        {
            try
            {
                CurrentSessionContext.Unbind(Session.SessionFactory); // always, also GC disposal
            }
            catch
            { }
            if (isDisposing) // explicit disposal, non-GC call only
            {
                _sessionManager.Dispose();
            }
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.