As my Italian friends probably know, I'm a huge NHibernate fan, as I decided to permanently adopt it in many desktop and web apps I've built or designed during the last 2-3 years; dealing with Session life cycle management in ASP.NET apps is pretty simple if you use well known patterns like
- Session per Request
- more rarely, Session per Conversation
In other words, you have a session alive and kickin' for the whole business transaction your WebForm is accomplishing and, being the NHibernate Session a Unit of Work implementation, this is absolutely faithful to that pattern definition.
For some reasons, using a similar approach with Linq To Sql's DataContext is not as easy. Let's take a look.
Dealing with detached objects
Let's start saying that associations in L2S are lazy fetched by default, so you need to have a context alive while navigating the entity graph if you don't want ObjectDisposedExceptions to be raised:
When adopting a Session per Request pattern, you must frequently detach/reattach the entity versus the current request's DataContext; however, attaching a detached entity in Linq To Sql is not as easy as it's supposed to be, as it isn't a supported scenario. I blogged in Italian a while ago about some strange reattaching behaviors I experienced in Beta2; RTM throws an exception if you try to reattach a persistent entity with associations not yet initialized, at least that's a more consistent behavior, although it doesn't help.
In other words, unless you don't want to deal with workarounds, you can't use Session per Request pattern with Linq To Sql.
Moving towards Session DataContext per Conversation
That brings us to the decision of having the same DataContext alive for the whole business transaction. I'm fine with that, although the architecture becomes a bit more complicated because its life should span several postbacks. After chatting a while with my friend Alk, I realized that ASP.NET's Cache is an awesome storage mechanism for DataContexts because:
- In a perfect world, every business transaction should be explicitely closed. In real world, however, users tend to close their browsers without notice. If we use sliding expiration, the ASP.NET's caching infrastructure takes care itself on getting rid of every pending DataContext that hasn't properly been disposed.
- Cache's sliding expiration is automatically renewed on every access.
- Cache provides a callback mechanism, so we can execute custom code when a DataContext has expired.
- We can configure Cache to not remove DataContexts when the system is in need of memory.
Instead of directly storing the DataContext, it's a better choice to wrap it in a generic UnitOfWork instance; this allows us to inject a bit of additional (and useful) logic:
The Items property, for example, is a string/object dictionary (an idea borrowed from NHibernate.Burrow) used to store custom transaction items that need to last till the transaction ends; I've also provided a Disposing event if the user wants to execute some custom code when a UnitOfWork is closed.
A UnitOfWorkContainer singleton object keeps track of all active UnitOfWorks and provides methods to start, rollback or commit (business) transactions:
Thanks to that, building a new UnitOfWork and storing it in the cache repository is only a matter of calling the BeginTransaction<T> (T is the Linq to Sql context's concrete type) of the UnitOfWorkContainer singleton class:
That method returns an ID which can be easily stored (f.e. using Viewstate) and will be used to retrieve the current UoW during the future postbacks. Now that a UoW has been created and stored into the container, we can retrieve it very easily
and we can use it to interact with the underlying Linq DataContext or with the Items storage as well; for example, the Page_Load method in my example contains the following code:
Thanks to this code, in every postback we have a reference to the person currently edited through the entity filed, we can freely navigate its object graph with the DataContext automatically querying the underlying database if it needs to lazily initialize any property, and tracking every change we make to any entity, being it the current Person instance or any associated one. Not bad, isn't it?
After some postbacks, here it comes the end of the conversation, in which the user is asked to say "Ok" and persist the changes, or say "No" and cancel everything; this ends the business transaction and invalidates the UnitOfWork:
Few details on what happens behind the curtain
The UnitOfWorkContainer class acts as a repository, providing methods to manage a UoW's lifetime and retrieve it given its Id. Once the user decides to begin a new business transaction, a new UoW is created and added to ASP.NET Cache:
Two details to notice here:
- The cache item is marked as NotRemovable, so it won't be cleared in case of memory shortage; it will disappear only after a 10 minutes sliding timeout or if explicitely removed.
- I've subscribed a CacheItemRemovedCallback to dispose the UoW once it has been removed from the cache (and, therefore, not reachable anymore from the client code)
Obviously this is only a very simple example, something very similar to what I used to do with NHibernate. For those who are interested on it as a starting point for their own apps, source code is available here.
