Ayende @ Rahien

It's a girl

Refactoring toward frictionless & odorless code: Limiting session scope

Originally posted at 3/30/2011

In the previous posts, we have looked at how we are going to setup an application using NHibernate. We set it up using:

    public MvcApplication()
    {
        BeginRequest += (sender, args) =>
        {
            CurrentSession = sessionFactory.OpenSession();
        };
        EndRequest += (o, eventArgs) =>
        {
            var session = CurrentSession;
            if (session != null)
            {
                session.Dispose();
            }
        };
    }

But this code is problematic. It is problematic because the session is open for the lifetime of the request. That is a problem, but probably not because of what you think. The #1 reason for issues like Select N+1 is people accessing lazy loaded properties in the views.

One good way of avoiding that is limiting the session scope only to the action, so when rendering the view, the session is not available. Therefor, every attempt to lazy load, will immediately throw. With ASP.Net MVC, this is very easy:

public class NHibernateActionFilter : ActionFilterAttribute
{
    private static readonly ISessionFactory sessionFactory = BuildSessionFactory();

    public static ISession CurrentSession
    {
        get { return HttpContext.Current.Items["NHibernateSession"] as ISession; }
        set { HttpContext.Current.Items["NHibernateSession"] = value; }
    }

    private static ISessionFactory BuildSessionFactory()
    {
        return new Configuration()
            .Configure()
            .BuildSessionFactory();
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        CurrentSession = sessionFactory.OpenSession();
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {

        var session = CurrentSession;
        if (session != null)
        {
            session.Dispose();
        }
    }
}

We need to modify the SessionController as well, so it would now be:

public class SessionController : Controller
{
     public HttpSessionStateBase HttpSession
     {
          get { return base.Session; }
     }

     public new ISession Session { get { return NHibernateActionFilter.CurrentSession; } }
}

Of course, we have broken the HomeController now, but we will discuss how in the next post.

Comments

François
04/08/2011 09:46 AM by
François

This implies we'll have different sessions for child actions. Is this benign?

The good point is thanks to the new ASP.Net MVC 3 Global Filters, we won't have to decorate every controller with the NHibernateActionFilterAttribute.

Scooletz
04/08/2011 10:37 AM by
Scooletz

@Fran├žois

Of course it is not. The scenario goes like:

  • parent action creates and stores a session in a context

  • child action creates and overrides parent's session in a context

  • child action disposes the session

  • the parent action is left with a handful of nothing, a disposed session.

The better solution is to use http modules with Castle disposing module (at the end of a request). It is fired once, at the end of parent request and allows you to use the same session (first level cache).

Ryan Heath
04/08/2011 12:35 PM by
Ryan Heath

@Scooletz

I think each child action creates its own session and disposes it. Also the parent action disposes its own session.

Because when child actions are invoked from the view the parent action has already disposed its session.

The sequence is like:

  • parent OnActionExecuting

  • parent Action

  • parent OnActionExecuted

  • viewResult is executed: any child action in the view goes through the same sequence.

// Ryan

Scooletz
04/08/2011 01:09 PM by
Scooletz

@Ryan,

yes, you're right! The child actions are executed when a parent's action result is executed. Your ordering is right. It does not change the fact, that first level cache optimization is lost, and that was the point behind my comment :)

Adam
04/08/2011 01:20 PM by
Adam

Sorry if this sounds thick, but is there a danger of overlapping requests sharing the same session and one disposing a session that is still being used? If that makes sense?

Scooletz
04/08/2011 01:28 PM by
Scooletz

@Adam,

Items are held by current request, so as far as I see your point: not, there can be no cross-request problem.

Dominic Pettifer
04/08/2011 01:45 PM by
Dominic Pettifer

Not sure I agree with the approach (in fact I don't), there are better ways to solve the Select N + 1 problem, such as strongly typed ViewModels. Plus in real world apps I build, I'm rarely sending just a collection domain models/entities to the View, it usually includes a bunch of other things, hence the argument for ViewModels.

Adam
04/08/2011 01:46 PM by
Adam

@Scooletz Thanks, that makes sense

Alwin
04/08/2011 02:26 PM by
Alwin

Look at the right, Future Post number 2 :)

joe
04/08/2011 02:31 PM by
joe

I'm still up in the air on how I'm going to handle session scope in my next project. It's nice to have it open in view for child actions, but this can lead to many select N+1 in view code if you're too lazy to map a domain object to viewmodel. Child actions are just sooo convenient for a complex site.master menu...so, it really comes down to not being lazy with your view models.

Dominic Pettifer
04/08/2011 02:53 PM by
Dominic Pettifer

@Alwin

Well spotted, I didn't see that :-)

bill
04/11/2011 10:19 AM by
bill

+1 for a view model. unless you're app is intended to be a super simple layer over db crud off course.

Arnis Lapsa
04/12/2011 02:41 PM by
Arnis Lapsa

Now that's a really good tip!

Comments have been closed on this topic.