[Ninject] Use one database session per view-model
By Michael DELVA on Monday 23 August 2010, 14:24 - C# - Permalink
TweetOne very useful web page I've read before beginning to code on my new project was this one: Data Access - Building a Desktop To-Do Application with NHibernate, from the well-known Ayende. In this article, among other hints and best-practices, he says, at the end of the Managing Sessions chapter:
The recommended practice for desktop applications is to use a session per form, so that each form in the application has its own session. Each form usually represents a distinct piece of work that the user would like to perform, so matching session lifetime to the form lifetime works quite well in practice. The added benefit is that you no longer have a problem with memory leaks, because when you close a form in the application, you also dispose of the session. This would make all the entities that were loaded by the session eligible for reclamation by the garbage collector (GC).
There are additional reasons for preferring a single session per form. You can take advantage of NHibernate’s change tracking, so it will flush all changes to the database when you commit the transaction. It also creates an isolation barrier between the different forms, so you can commit changes to a single entity without worrying about changes to other entities that are shown on other forms.
While this style of managing the session lifetime is described as a session per form, in practice you usually manage the session per presenter.
We'll see now how to implement this behavior, with Caliburn as the MVVM client framework, and Ninject as the dependency injector.
In order to make this article easier to read and understand, I won't use the real interfaces, like NH's ISession and ISessionFactory, or Caliburn's IScreen. Instead, I will create again these interfaces and classes in their simplest expression, so we can focus on the important things.
So let's begin by the NH interfaces first:
public interface INHibernateSessionFactoryBuilder
{
ISessionFactory GetSessionFactory();
}
public class NHibernateSessionFactoryBuilder : INHibernateSessionFactoryBuilder
{
private ISessionFactory sessionFactory;
public ISessionFactory GetSessionFactory()
{
return sessionFactory ?? (sessionFactory = CreateSessionFactory());
}
private ISessionFactory CreateSessionFactory()
{
return new SessionFactory();
}
}
public interface ISessionFactory
{
ISession OpenSession();
}
public class SessionFactory : ISessionFactory
{
public ISession OpenSession()
{
return new Session();
}
}
public interface ISession : IDisposable
{
Guid Id { get; }
}
public class Session : ISession
{
private readonly Guid id = Guid.NewGuid();
public Guid Id { get { return id; } }
public void Dispose()
{
}
}
Nothing fancy here. I just have the interface INHibernateSessionFactoryBuilde which creates a new ISessionFactory, which will be used to open a new ISession. In a real application, the function INHibernateSessionFactoryBuilde.CreateSessionFactory would use for example Fluent NHibernate to instantiate the SessionFactory. You have noticed that I have added a Id property to ISession, in order to compare easily if 2 sessions are the same.
I've chosen to use the Repository Pattern to manage the entities in my application. Let's just create a fake repository:
public interface IRepository
{
ISession Session { get; }
}
public class Repository : IRepository
{
private readonly ISession session;
[Inject]
public Repository(ISession session)
{
this.session = session;
}
public ISession Session { get { return session; } }
}
You have noticed the Inject attribute, which notifies Ninject to apply dependency injector, to inject an instance of ISession when it will be asked to create an instance of IRepository.
Let's finish with the view-model interfaces and classes:
public interface IScreen : IDisposable
{
}
public abstract class BaseScreen : IScreen
{
private readonly ISession session;
protected BaseScreen(ISession session)
{
this.session = session;
}
public ISession Session
{
get { return session; }
}
public void Dispose()
{
session.Dispose();
}
}
public class Screen1 : BaseScreen
{
private readonly IRepository repository;
[Inject]
public Screen1(ISession session, IRepository repository)
: base(session)
{
this.repository = repository;
}
public IRepository Repository
{
get { return repository; }
}
}
public class Screen2 : BaseScreen
{
private readonly IRepository repository;
[Inject]
public Screen2(ISession session, IRepository repository)
: base(session)
{
this.repository = repository;
}
public IRepository Repository
{
get { return repository; }
}
}
We just have an IScreen interface, which will be implemented by a base view-model class, which itself is derived in 2 screens. You can note that BaseScreen needs an instance of ISession in its constructor, so it can manage the session's lifecycle. This instance will be passed by Screen1 and Screen2, which will get it by injection, thanks again to the Inject attribute.
I use Ninject to create the view-models to mimic the behavior of Caliburn, which create all the view-models instances with the dependency injector.
Now that our infrastructure is in place, let's create our NinjectModule, to add the bindings to the Ninject kernel:
public class ScreensModule : NinjectModule
{
public override void Load()
{
Bind<Screen1>().ToSelf();
Bind<Screen2>().ToSelf();
Bind<IRepository>().To<Repository>();
}
}
public class NHibernateModule : NinjectModule
{
public override void Load()
{
Bind<INHibernateSessionFactoryBuilder>()
.To<NHibernateSessionFactoryBuilder>()
.InSingletonScope();
Bind<ISession>().ToMethod(ctx =>
{
var session = ctx.Kernel.Get<INHibernateSessionFactoryBuilder>()
.GetSessionFactory()
.OpenSession();
return session;
});
}
}
The former module registers our screens and the repository, while the latter registers the classes needed to create a NH session. This is the kind of code you can find everywhere on internet after a bit of googling. When asked to create a session, Ninject will first resolve the INHibernateSessionFactoryBuilder interface, which will create the ISessionFactory instance, and then call OpenSession on this instance. You can see that INHibernateSessionFactoryBuilder is resolved using a singleton, because in a real application, this step will create all the mappings for the entities, and as such may be a long operation. So you want it to be executed one time for the entire life cycle of your application. And ISession is resolved with the ToMethod function, so that it is called each time a new ISession is needed.
It's now time to write our unit-tests. But before that, let's state our needs:
- two view-models of different types must have different sessions
- two view-models of the same type must have different sessions
- each injected class which needs a session, and is injected in a view-model, must have the same session as the view
This results in these three tests:
[Fact]
public void TwoDifferentScreensHaveDifferentSessions()
{
IKernel kernel = new StandardKernel(new NHibernateModule(),
new ScreensModule());
var sc1 = kernel.Get<Screen1>();
var sc2 = kernel.Get<Screen2>();
Assert.NotEqual(sc1.Session.Id, sc2.Session.Id);
}
[Fact]
public void OneScreenInstantiatedTwoTimesHaveDifferentSessions()
{
IKernel kernel = new StandardKernel(new NHibernateModule(),
new ScreensModule());
var sc1a = kernel.Get<Screen1>();
var sc1b = kernel.Get<Screen1>();
Assert.NotEqual(sc1a.Session.Id, sc1b.Session.Id);
}
[Fact]
public void OnlyOneSessionIsUsedWithinAScreen()
{
IKernel kernel = new StandardKernel(new NHibernateModule(),
new ScreensModule());
var sc1 = kernel.Get<Screen1>();
Assert.Equal(sc1.Session.Id, sc1.Repository.Session.Id);
}
I just use the Id property of our fake ISession interface to compare the sessions between the views and repositories.
Here are the results of the tests:
We do have one instance of ISession for each of our view-models, but we also have one instance for each of our repository instances.
This is normal, as we have configured Ninject to open a new session each time it must resolve ISession. We then have to tweak the behavior of Ninject.
Welcome to the most interesting part of this article :)
To manage the creation of the instances of the classes, Ninject uses the notion of Scope. There are 4 different built-in scopes, as stated here. As no one of the existing scopes fits our need, we are going to use the InScope method, which will allow us to use our own scope.
This method takes one parameter, of type Func<IContext, object>. The object we are going to return for this Func will be used internally by Ninject. Indeed, when Ninject is asked to create an instance of a registered type, it looks in a hashtable if there already is an object matching the instance the scope returned. If there is, then Ninject will return the instance of the registered type, associated to the key. If there isn't, then Ninject will create a new instance of the type, and add a new entry in the hashtable, with the scoped object as key, and the newly created type as value.
For example, the InTransientScope method will always create a new instance of the requested type, because the scope it returns is always a new object. The InThreadScope method keeps as a key the thread id, and so on...
So, what we need to do, is to make the scope function always return the same object when a ISession is asked inside a view-model. In our preceding example, we must ensure that the injection of the ISession instance when we create Screen1 uses the same scope for both Screen1 and the repository.
To achieve that, we will use the IContext parameter of the InScope method, and here is the wiki page where you can learn a bit more about it.
The point of interest of this IContext instance is its Request property, which is of type IRequest. This property will contain all the informations Ninject needs when it must create and inject a type. The properties of IRequest we are going to examine are the Service property, which is of type Type and is the type of the object Ninject must create, and the ParentRequest property, which you can use recursively to walk up the activation tree.
Let's write this chunk of code to understand how the activation is done:
static void Main(string[] args)
{
IKernel kernel = new StandardKernel(new NHibernateModule(),
new ScreensModule());
var sc1 = kernel.Get<Screen1>();
Console.ReadKey();
}
}
public class ScreensModule : NinjectModule
{
public override void Load()
{
Bind<Screen1>().ToSelf();
Bind<IRepository>().To<Repository>();
}
}
public class NHibernateModule : NinjectModule
{
public override void Load()
{
Bind<INHibernateSessionFactoryBuilder>()
.To<NHibernateSessionFactoryBuilder>()
.InSingletonScope();
Bind<ISession>().ToMethod(ctx =>
{
var session = ctx.Kernel.Get<INHibernateSessionFactoryBuilder>()
.GetSessionFactory()
.OpenSession();
return session;
})
.InScope(ctx =>
{
var request = ctx.Request;
Console.WriteLine(request.Service.Name);
string tab = "\t";
while ((request = request.ParentRequest) != null)
{
Console.WriteLine("{0}From {1}", tab, request.Service.Name);
tab += tab;
}
return new object();
});
}
}
Here is the output of this small program:
ISession
From Screen1
ISession
From Screen1
ISession
From Screen1
ISession
From Screen1
ISession
From IRepository
From Screen1
ISession
From IRepository
From Screen1
ISession
From IRepository
From Screen1
ISession
From IRepository
From Screen1
You can see here that Ninject creates the session needed by the constructor of Screen1, and then creates the session needed by the IRepository, itself needed by the constructor of Screen1. And the interesting part of this, is that all of these activations have a common topmost request service, which is the Screen1 type.
Looks like we may have found our common scope for the session, don't you think? Well, almost. Because if we use the top parent service type, our second test (OneScreenInstantiatedTwoTimesHaveDifferentSessions) will fail, because the two created instances, as they are of the same type, will have the same scope, and then Ninject will return the same session object.
So we must find another scope. And I won't let the suspense go any longer: we will use the highest Request in the activation tree, whose Service property is a type which derives from IScreen. Indeed, when the Screen1 instance will be created, all injected types in this instance (the ISession in the constructor of the screen, the IRepository of the constructor, and the ISession of the IRepository) will all have the same parent request, making it the perfect object to be returned in our InScope method.
And here is the final code of our ISession activation:
Bind<ISession>().ToMethod(ctx =>
{
var session = ctx.Kernel.Get<INHibernateSessionFactoryBuilder>()
.GetSessionFactory()
.OpenSession();
return session;
})
.InScope(ctx =>
{
var request = ctx.Request;
if (typeof(IScreen).IsAssignableFrom(request.Service))
return request;
while ((request = request.ParentRequest) != null)
if (typeof(IScreen).IsAssignableFrom(request.Service))
return request;
return new object();
});
Let's run the tests to see if they all succeed:
Et voilà!