I know that there are a few blog posts out there on this subject, so I feel like I'm repeating myself while writing this. Bear with me...please. Over the last month I've been chasing an ISession corruption error in our application with little success. During that time I spent a lot of time looking at our WCF implementation to ensure that it wasn't some piece of that which was causing the problem. Needless to say, it wasn't, but I have a much greater appreciation for the extensibility of WCF as a result. Part of my research/debugging/hacking around/crying was looking into the most appropriate way to create, persist and end a nHibernate ISession object during each individual WCF OperationContract call. Here's what I figured out and created.
The first, and easiest thing that you need to do is set the ServiceContract Attribute to have an InstanceContextMode = InstanceContextMode.PerCall. The key here is that I want per call isolation. Choosing thie InstanceContextMode will force the creation of a new service context for each client request and ultimately create a new InstanceContext object for each call. What you're doing here is telling WCF that every call to an OperationContext should be treated as a distinctly separate code execution area. If you were to leave InstanceContextMode to its default (InstanceContextMode.PerSession) the execution area of the code would create an InstanceContext for the duration of the Channel's life. Basically, that would mean that creating a connection to the WCF service would create a new InstanceContext and all OperationContracts would have access to it until the channel was released. This will allow you to access items in the InstanceContext across calls, which is not how we want to handle our ISessions.
[ServiceContract] [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] [NHibernateContext] public interface ICustomerServices { [OperationContract] IEnumerable<CustomerListingDto> RetrieveListingOfAll(); }
So we've created a service that is set to have PerCall InstanceContexts. This is the first step to creating and storing the ISession. Putting an ISession into the InstanceContext requires us to create a custom extension using IExtension<T>. All our extension needs to do is provide a place to store the ISession object for that OperationContract call. The extensions on the InstanceContext have a lifetime that lasts only for the duration of the call. In the end the extension looks like this:
public class NHibernateContextExension : IExtension<InstanceContext> { public NHibernateContextExension(ISession session) { Session = session; } public ISession Session { get; private set; } public void Attach(InstanceContext owner) {} public void Detach(InstanceContext owner) {} }
Now that we have that extension we need to get it attached to each and every OperationContract call that is handled by our service. An IInstanceContextInitializer implementing class is used to trigger the addition of the extension to the InstanceContext. Every time that an InstanceContext is initialized (which will be on every Operation call since we're using InstanceContextMode.PerSession) the NHibernateContextExtension is added to the newly created InstanceContext. Also note that it is at this point that the NHibernate ISession object is created and assigned to the InstanceContext's extension. It's here that the nHibernate session per operation call concept is implemented.
public class NHibernateContextInitializer : IInstanceContextInitializer { public void Initialize(InstanceContext instanceContext, Message message) { instanceContext.Extensions.Add( new NHibernateContextExtension( NHibernateFactory.OpenSession() ) ); } }
You probably noticed in the NHibernateContextInitializer class above that the ISession object is being created from a NHibernateFactory object. This object is a wrapper for the native NHibernate ISessionFactory object. We need it implemented in this way so that we have a technique for accessing a session factory that is created outside of the scope of this little framework. Later in this post you will see how this object is used and initialized with a valid SessionFactory object.
public static class NHibernateFactory { private static ISessionFactory _sessionFactory; public static void Initialize() { Initialize(new Configuration().Configure().BuildSessionFactory()); } public static void Initialize(ISessionFactory sessionFactory) { _sessionFactory = sessionFactory; } public static ISession OpenSession() { return _sessionFactory.OpenSession(); } }
With a way to attach a new ISession to every InstanceContext that is created, we need to trigger this process on each and every call. To do that we will attribute the WCF service with a custom attribute. All we need to do is create a class that is an Attribute and implements the IContractBehavior interface. During ApplyDispatchBehavior we add an NHibernateContextIntializer to start the entire process of creating the new NHibernate ISession for each call.
public class NHibernateContextAttribute : Attribute, IContractBehavior { public ISessionFactory SessionFactory { private get; set; } public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint) {} public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime) { dispatchRuntime.InstanceContextInitializers.Add( new NHibernateContextInitializer()); } public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime) {} public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) {}
Simply creating a new ISession for each call to the WCF service isn't enough. We need to be able to work with that ISession object in our data access endeavours. To do that I've created a simple class with a static method on it that retrieves the NHibernateContext Extension from the current InstanceContext. This is possible because there is only one InstanceContext during the life of each service call. To make this fall in line with other 'Context' object that we are used to working with (like HttpContext) I've created the static method called Current.
public class NHibernateContext { public static NHibernateContextExension Current() { return OperationContext.Current. InstanceContext.Extensions. Find<NHibernateContextExension>(); } }
All of that explanation is well and good, but the real trick is how the hell do you use this. That is fairly simple. You need to do three things. First you need to create and initialize a nHibernate SessionFactory and inject it into the custom wrapper that is provided in this code. Normally you would want to do this once for the life of the application as shown by placing this code in the Application_Start of the global.asax file. This code snippet shows two options for initializing the factory. The first line will automatically load a nHibernate SessionFactory using the hibernate.cfg.xml file that is in the same folder as the IglooCoder.Commons.dll file. The second line allows more flexibility to initialize a nHibernate SessionFactory in the way that you need or want.
protected void Application_Start(object sender, EventArgs e) { NHibernateFactory.Initialize(); //or// NHibernateFactory.Initialize(new Configuration().Configure().BuildSessionFactory()); }
Second you need to attribute any services with the NHibernateContextAttribute) that will need to access data using nHibernate.
[ServiceContract] [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] [NHibernateContext] public interface ICustomerServices { [OperationContract] IEnumerable<CustomerListingDto> RetrieveListingOfAll(); }
The final step is to make use of the ISession object that the framework is creating for each call. As you can see in the code below there is a bit of a trick to using t
public abstract class BaseRepository<T> : IRepository<T> { public T FetchById(int id) { return NHibernateContext.Current().Session.Get<T>(id); } }
If you want to use this code and not have to write it all out you can grab the source from the project's Subversion trunk (https://igloocoder.net:8445/svn/IglooCommons/trunk/). Compile the code running b.bat at the command line and you will find the compiled assemblies in the /compile folder.
My next post will talk about how to maintain testability while using this stuff.
Ideas for implementing this came from Dan Rigsby's posts on WCF extension here and here.
posted @ Monday, July 21, 2008 8:16 PM