If you've used NHibernate in a ASP.NET MVC project you probably encountered the problem of keeping session open in the view in order to retrieve model data. (even though if session is open then the view actually makes calls to the db under the hood, which maybe is not very MVC. But that's another story)
The idea is to pass controllers an instance of a NHibernate session which is unique for the http request.
First off we need to "hijack" the ASP.NET mechanism that instantiate controllers to pass something in the constructor. In this case it's a ISession, but it could well be a repository class (which in turn gets a ISession passed in its constructor).
So a controller would look like this:
public class MyController : Controller
{
ISession session;
public MyController(ISession session)
{
this.session = session;
}
...
I've used Unity dependency container to manage the instantiation of controllers. ASP.NET MVC lets you do that by overriding the DefaultControllerFactory.
This is as simple as writing this class:
public class UnityControllerFactory : DefaultControllerFactory
{
UnityContainer container;
public UnityControllerFactory(UnityContainer container)
{
this.container = container;
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
IController controller = null;
if (controllerType != null)
{
controller = this.container.Resolve(controllerType) as IController;
}
return controller;
}
}
To make the session instance "singleton" for the http request, all I need to do is write a simple custom lifetime manager for Unity:
public class PerRequestLifetimeManager : LifetimeManager
{
public const string Key = "SingletonPerRequest";
public override object GetValue()
{
return HttpContext.Current.Items[Key];
}
public override void SetValue(object newValue)
{
HttpContext.Current.Items[Key] = newValue;
}
public override void RemoveValue() {} // this does not appear to be used ...
}
Let's get everything together in Global.asax. We need to register NHibernate ISession in Unity configuration. It should inject it using a factory method (OpenSession method of a ISessionFactory instance).
Then we need to tell MVC to use our Controller factory, passing the Unity container of our ASP.NET MVC application.
To close open NHibernate sessions we add some code in the Application_EndRequest.
Here's the code to put in our Global.asax file.
protected void Application_Start()
{
this.container = new UnityContainer();
ISessionFactory sessionFactory = NHConfigurator.CreateSessionFactory();
// NHConfigurator is my helper conf class
container.RegisterType(
new PerRequestLifetimeManager(),
new InjectionFactory(c => {
return sessionFactory.OpenSession();
}));
ControllerBuilder.Current.SetControllerFactory(new UnityControllerFactory
(this.container));
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
protected void Application_EndRequest(object sender, EventArgs e)
{
Object sessionObject = HttpContext.Current.Items[PerRequestLifeTimeManager.Key];
if (sessionObject != null)
{
ISession currentSession = sessionObject as ISession;
if (currentSession != null)
{
currentSession.Close();
}
}
}
Hope this helps!
Nice & interesting post Giorgio! Tnx!
ReplyDeleteA question: why Unity as DI?
Thanks PadovaBoy! ;)
ReplyDeleteI like Unity because of the nice and simple syntax. I tried it and works really nice and it's not different from other DI containers.
Plus, it's made by MS. So I expect some more integration with other MS products and some software shops are kind of "attached" to MS (not very Alt.net) so selling Unity to them should be easier...
Nice post Giorgio!
ReplyDeleteAnd if you use Autofac as DI you write:
ReplyDeletevar builder = new ContainerBuilder();
builder.Register(c => NHConfigurator.CreateSessionFactory()).SingleInstance();
builder.Register(c => c.Resolve
().OpenSession()).InstancePerLifetimeScope();
var ContainerProvider = new ContainerProvider(builder.Build());
ControllerBuilder.Current.SetControllerFactory(new AutofacControllerFactory(ContainerProvider));
Thank you Mare, it's interesting to see it implemented with Autofac. Maye it's because I'm used to Unity but I find it a little less readable. I guess "InstancePerLifetimeScope" method sets lifetime to the life of the http request.
ReplyDeleteJust curious, what's the biggest benefit on doing this through Unity when you could have just created the session (through your helper) manually and put it into HttpContext.Current.Items manually with a given key on Application_StartRequest,
ReplyDeletethen in your controllers just made a convenience getter to the session which fetched it from HttpContext.Current.Items with the same key.
then in Application_End just end the session
The benefit are that you can unit test the controllers, both passing real sessions or mocked sessions. Plus, if instead of using directly Nhibernate sessions in the controllers, you want to use some repository class (perhaps living even in a separate project/assembly), if this repository class gets a session in it's constructor, Unity will still inject an open session in it. You avoid keeping global variables floating around and perhaps even leaking out of the project into external projects, thus coupling things. Last but not least.... it's just plain cleaner and more elegant.
ReplyDeleteDo you read your blog after you post it? do you find problem reading code? It just made me blind for few minutes after I gone through horrible color of code. Worth consider making your website reader friendly. Thanks for sharing tips though.
ReplyDeleteYes, the code is not very readable, but that's just the Blogger theme... now I've changed it, it's still far from perfect (or nice) but I'll stick with it for the moment.
ReplyDeleteYou have your NHibernate session object in the controller constructor, that means all your business logic and database calls are made from your controller actions? Isn't this the same as dumping everything in your button click events in the old asp.net? Doesn't this make it difficult to refactor and unit test, and other issues when you violate SOLID programming principles?
ReplyDeleteDear Anonymous reader...
ReplyDeleteThank you for reading my blog post.
Perhaps, if you've read it more carefully you would have noticed this sentence:
" In this case it's a ISession, but it could well be a repository class (which in turn gets a ISession passed in its constructor)."
This means that, without changing anything other the controller collaborator from ISession to IAnyClasscontainingLogicOrWhatever (or even not an interface but a concrete class), you would still have MVC using Unity to inject the session, this time to the logic/repository class instead of directly in the controller.
It was chosen to inject directly for simplicity, otherwise code would have been a lot less readable.
I wonder how you think you can apply SOLID principle without using a dependency injection framework, anyway.
If I was not clear I will be happy to explain it to you, perhaps in a new blog post.
Where's the configurator?
ReplyDeleteIt looks like you are calling CreateSessionFactory on each request. I would think you'd want to register your session factory with a ContainerControlledLifetimeManager. I do something like:
ReplyDeletecontainer.RegisterType<ISessionFactory>(new ContainerControlledLifetimeManager(), c => CreateSessionFactory());
container.RegisterType<ISession>(new PerRequestLifetimeManager(), c => c.Resolve<ISessionFactory>().OpenSession());
Mike, you are right and in fact in my projects I'm currently instantiating the factory outside the call to InjectionFactory, the code in the post is "obsolete".
ReplyDeleteI'll correct it by simply moving the line:
ISessionFactory sessionFactory = NHConfigurator.CreateSessionFactory()
a couple of lines upwards, and it will do the trick.
Thanks for pointing out the issue.
Very nice and this is exactly what I am looking for.
ReplyDeleteThere is another question though, who is responsible to recycle NH session with this implementation, GC or something else?
Thanks Hardy!
ReplyDeleteSession is closed on Application_EndRequest, that should dispose it.
Giorgio, I think I may need to use your solution, but I am only now learning about NHibernate and MVC. In your opening paragraph you describe a problem with the View accessing the the database through the Model. I was under the impression that all database communication would happen in the Model, and not in the View, so I don't understand why this problem scenario would occur. It would be great if you could provide (or link to) code example of the problem. Thank you.
ReplyDeleteAlex, in the MVC framework the View is supposed to just show (a view of) the data and not actually making anything, let aside doing calls to a database. Plus, a good practice is to make "deliberate" calls to the database and to "accidental" so as to control also performance issues.
ReplyDeleteThe purpose of having a single nhibernate session for every request is that a lot of the nice features (transation, caching) of Nhibernate are possible if the db calls are made inside of the same session. Also I believe that having multiple sessions in the same request can be a problem with some rdbms, for instance with SqlLite.
So the correct pattern is having one session per request and passing that session (directly or "indirectly") to any dependency in our code.
Thanks for commenting and have a good time with MVC and NHibernate!