NHibernate Sessions + Transactions Using ASP.NET M

2019-03-31 22:01发布

问题:

I'm writing a new application at the moment, although company standard is to use NHibernate (because that's standard across all projects), and instead I'm using ASP.NET MVC 3 due to its maturity now. I've implemented my transactions in the controllers (which supposedly is the way you're supposed to do it), so it looks like this in my root controller:

[TransactionPerRequest]
public class FbsController : Controller
{

}

Then all my controllers inherit from this FbsController. The reason this is done is because 90% of all my actions will be going off to the database, so the overhead for creating a transaction and disposing of it for the remainder 10% of actions (which will rarely be executed) isn't worth decorating each action with [TransactionPerRequest].

The thing that's always stumped me is in regards to NHibernate sessions. In the repository classes this along the lines of something I have, although this is different in other projects:

    public void Add(User user)
    {
        using (ISession session = NHibernateHelper.OpenSession())
        {
            session.Save(user);
        }
    }

    public void Remove(User user)
    {
        using (ISession session = NHibernateHelper.OpenSession())
        {
            session.Delete(user);
        }
    }

    public User GetById(int userId)
    {
        using (ISession session = NHibernateHelper.OpenSession())
        {
            return session.QueryOver<User>()
                .Where(c => c.UserID == userId)
                .SingleOrDefault();
        }
    }

So then for the majority of functions in my repository I'm having to open a session. Is there any way of avoiding this behaviour so I don't have to open a session inside each and every repository method? It seems a bit counter-intuitive as I'll usually have to do it for every single one. I was wondering what everyone else's solution was to the transaction & session issue that I see littered around code in a variety of ways.

Realistically I want my repository methods to look like the following:

    public void Add(User user)
    {
        session.Save(user);
    }

    public void Remove(User user)
    {
        session.Delete(user);
    }

    public User GetById(int userId)
    {
        return session.QueryOver<User>()
            .Where(c => c.UserID == userId)
            .SingleOrDefault();
    }

With everything being dealt with implicitly.

回答1:

You might take a look at the following series of blog posts by Ayende Rahien:

  1. Refactoring toward frictionless & odorless code: The baseline
  2. Refactoring toward frictionless & odorless code: Hiding global state
  3. Refactoring toward frictionless & odorless code: Limiting session scope
  4. Refactoring toward frictionless & odorless code: A broken home (controller)
  5. Refactoring toward frictionless & odorless code: The case for the view model
  6. Refactoring toward frictionless & odorless code: Getting rid of globals
  7. Refactoring toward frictionless & odorless code: What about transactions?


回答2:

I do something along the lines of:

In my Global.asax.cs:

public static ISessionFactory SessionFactory { get; set; }

and then define in Application_Start:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);

    var nhConfig = new Configuration().Configure();
    SessionFactory = nhConfig.BuildSessionFactory();
}

This class is then created:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NHSession : ActionFilterAttribute
{
    public NHSession()
    {
        Order = 100;
    }

    protected ISessionFactory sessionFactory
    {
        get
        {
                return MvcApplication.SessionFactory;
        }
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var session = sessionFactory.OpenSession();
        CurrentSessionContext.Bind(session);
        session.BeginTransaction();
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        var session = CurrentSessionContext.Unbind(sessionFactory);
        if (session != null)
        {
            if (session.Transaction.IsActive)
            {
                try
                {
                    session.Transaction.Commit();
                }
                catch
                {
                    session.Transaction.Rollback();
                }
            }
            session.Close();
        }
    }
}

Then my generic repository looks something along the lines of:

public class Repository<T> : IRepository<T>
{
    private readonly ISessionFactory SessionFactory;
    public Repository(ISessionFactory sessionFactory)
    {
        SessionFactory = sessionFactory;
    }
    public ISession Session
    {
        get
        {
            return SessionFactory.GetCurrentSession();
        }
    }
    public T Get(long id)
    {
        return Session.Get<T>(id);
    }
}

My concrete implementation of a repository is:

public class CmsContentRepository : Repository<CmsContent>, ICmsContentRepository
{
    public CmsContentRepository(ISessionFactory sessionFactory) : base(sessionFactory) { }
}

And one more thing I then decorate my controllers like so:

[NHSession]
public ViewResult Revisions(int id)
{
    var model = Service.CmsContentRepository.Get(id);
    return View("Revisions", model);
}

This then gives me the ability to use a unit of work across a request. Basically a request comes in and starts a session, the SessionFactory is passed into the constructor of the repository(ies). I do use DI here but that is optional. If an error is detected then the session is rolled back if not it is committed on the end of a request. I would recommend NHProf as it helps you understand with session management (that is if it is not set up correctly).



回答3:

I used StructureMap to automatically start a session when ISession is first called, then cache the session by HttpRequest. This lets me use lazy loading and transactions throughout the request with a minimum of coding fuss.

Here's the code for my bootstrapper which sets everything up for me using Fluent NHibernate and StructureMap.

public class Bootstrapper
{
    public static ISessionFactory DBSessionFactory { get; set; }
    public static ISession DBSession { get; set; }

    public static void InitializeObjectFactory()
    {
        ObjectFactory.Initialize(x =>
                                     {
                                         x.PullConfigurationFromAppConfig = true;
                                         x.Scan(y =>
                                                    {
                                                        y.Assembly(Assembly.GetAssembly(typeof(AccountController)));
                                                        y.Assembly(Assembly.GetAssembly(typeof(IMyProject)));
                                                        y.WithDefaultConventions();
                                                    }
                                             );

                                         // these are for NHibernate
                                         x.ForRequestedType<ISessionFactory>()
                                             .CacheBy(InstanceScope.Singleton)
                                             .TheDefault.Is.ConstructedBy(GetDBSessionFactory);

                                         // open session at beginning of every http request 
                                         // (the session is disposed at end of http request in global.asax's Application_EndRequest)
                                         x.ForRequestedType<ISession>()
                                             .CacheBy(InstanceScope.HttpContext)
                                             .TheDefault.Is.ConstructedBy(GetDBSession);
                                     });
    }

    public static ISessionFactory CreateSessionFactory()
    {
        return GetFluentConfiguration()
            .BuildSessionFactory();
    }

    public static ISessionFactory GetDBSessionFactory()
    {
        if (DBSessionFactory == null)
        {
            DBSessionFactory = CreateSessionFactory();
        }
        return DBSessionFactory;
    }

    public static ISession GetDBSession()
    {
        if (DBSession == null)
        {
            DBSession = CreateSession();
        }
        return DBSession;
    }

    public static ISession CreateSession()
    {
        return GetDBSessionFactory()
            .OpenSession();
    }

    public static FluentConfiguration GetFluentConfiguration()
    {
        string commandTimeout = ConfigurationManager.AppSettings["MyDBCommandTimeout"];
        return Fluently.Configure()
            .Database(// use your db configuration )
            .Mappings(m =>
                          {
                              m.HbmMappings
                                  .AddFromAssemblyOf<MyEO>();
                              m.FluentMappings
                                  .AddFromAssemblyOf<MyEO>()
                                  .AddFromAssemblyOf<MyEOMap>();
                          })
            .ExposeConfiguration(
                cfg =>
                    {
                        // command_timeout sets the timeout for the queries
                        cfg.SetProperty("command_timeout", commandTimeout);
                    }
            );
    }
}

Call Bootstrapper.InitializeObjectFactory(); in the Application_Start() method in your global.asax, like this:

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
    Bootstrapper.InitializeObjectFactory();
    ...
}

Close the session in your Application_EndRequest():

protected void Application_EndRequest()
{
    // ensure that we aren’t leaking ISessions on every web request
    if (Bootstrapper.DBSession != null)
    {
        if (Bootstrapper.DBSession.IsOpen)
        {
             Bootstrapper.DBSession.Close();
        }
        Bootstrapper.DBSession.Dispose();
        Bootstrapper.DBSession = null;
    }

    HttpContextBuildPolicy.DisposeAndClearAll();
}

Now you just call

ObjectFactory.GetInstance<ISession>() 

from anywhere (I wrap it in a helper class to keep my code simple) and StructureMap will give you your cached session.