Often, a unit of work maps neatly on to a single controller action. I'll show you how to create an action filter to manage our NHibernate sessions in an ASP.NET MVC application.
Setup an ASP.NET MVC application for NHibernate. The steps are as follows:
NHibernate.dll
, NHibernate.ByteCode.Castle.dll
, log4net.dll
, and our Eg.Core
project from Chapter 1.web.config
file, set up the NHibernate and log4net configuration sections. Refer to the Configuring NHibernate with App.config recipes in Chapter 2.current_session_context_class
property to web
.Global.asax
, create a static property named SessionFactory
.public static ISessionFactory SessionFactory { get; private set; }
Application_Start
method, add this code.log4net.Config.XmlConfigurator.Configure(); var nhConfig = new Configuration().Configure(); SessionFactory = nhConfig.BuildSessionFactory();
NHibernateSessionAttribute
class as shown in the following code:[AttributeUsage(AttributeTargets.Method, AllowMultiple=false)] public class NHibernateSessionAttribute : ActionFilterAttribute { public NHibernateSessionAttribute() { Order = 100; } protected ISessionFactory sessionFactory { get { return MvcApplication.SessionFactory; } } public override void OnActionExecuting( ActionExecutingContext filterContext) { var session = sessionFactory.OpenSession(); CurrentSessionContext.Bind(session); } public override void OnActionExecuted( ActionExecutedContext filterContext) { var session = CurrentSessionContext.Unbind(sessionFactory); session.Close(); } }
[NHibernateSession] public ActionResult Index() { return View(DataAccessLayer.GetBooks()); }
using System.Collections.Generic; namespace ActionFilterExample { public static class DataAccessLayer { public static IEnumerable<Eg.Core.Book> GetBooks() { var session = MvcApplication.SessionFactory .GetCurrentSession(); using (var tx = session.BeginTransaction()) { var books = session.QueryOver<Eg.Core.Book>() .List(); tx.Commit(); return books; } } } }
Views
folder, create a folder named Book
Book
folder, add a view using the settings shown in the following screenshot:USE NHCookbook INSERT INTO Product VALUES ( NEWID(), 'Eg.Core.Book', 0, 'NHibernate 3 Cookbook', 'Bridging the gap between database and .NET Application', 45.99, null, 'Jason Dentler', '3043' ) INSERT INTO Product VALUES ( NEWID(), 'Eg.Core.Book', 0, 'NHibernate 2 Beginner's Guide', 'Rapidly retrieve data from your database into .NET objects', 45.99, null, 'Aaron Cure', '978-1-847198-90-7' )
The concept behind this recipe is very similar to our session-per-request recipe at the beginning of the chapter. We are using NHibernate's contextual sessions with a variation of session-per-request.
Before the Index()
controller action is executed, ASP.NET MVC will run our filter's OnActionExecuting
method. In OnActionExecuting
, our action filter opens a session and binds it to this web request using NHibernate's contextual sessions feature.
Similarly, ASP.NET MVC will run our filter's OnActionExecuted
when Index()
returns. In OnActionExecuted
, the filter unbinds the session and closes it. Then, ASP.NET MVC processes the action result. In this case, it renders a view to display a list of books.
The Order
property of an action filter determines in what order that action filter executes. For Executing
events, all action filters with an unspecified Order
are executed first. Then, those with a specific Order
are executed, starting with zero, in ascending order. For Executed
events, the process works in reverse. Essentially, it allows us to stack action filters - last in, first out. This provides a determinate order, so we can combine it with session-dependent filters with higher Order
values.
NHibernate requires an NHibernate transaction around every database interaction, whether it be a direct method call on the session or an action that triggers lazy loading. With this implementation, it is very difficult to capture lazy loading calls in a transaction. As we will see in the next recipe, we can combine the proper use of sessions and transactions in a single action filter to allow for lazy loading elsewhere in the controller action.
Be sure an action loads all of the data required by the view. The session is not open anymore when the action result (a view, in this case) is rendered.
Even with more lenient implementations, it's not recommended to access the database from the view. Views are usually dynamic and difficult to test.
To avoid this issue and many others, many ASP.NET MVC applications use view models. A view model class is defined for each view, and contains exactly the data required by that view, and nothing more. Think of it as a data transfer object between the controller and the view.
Rather than write pages of plumbing code to copy data from entities to view models, you can use an open source project, AutoMapper. When combined with an action filter attribute, this process becomes dead simple. A good example of this can be found in Jimmy Bogard's blog post at http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/06/29/how-we-do-mvc-view-models.aspx.
Pay attention to the Order
property on the AutoMapper attribute. To allow for lazy loading when translating from entities to view models, the Order
should be even higher than our session attribute. This ensures that the session is open when AutoMapper is translating.