Unit of Work in Rich Clients

When it comes to dealing with databases, smart .NET developers follow the unit of work pattern. In NHibernate, the unit of work is an ISession. In LINQ to SQL and Entity Framework, it's the DataContext/DbContext.

When implementing a unit of work, or in fact any object, lifetime matters. When is the unit of work created? When does it end?

When writing ASP.NET or WCF applications, the lifetime of a unit of work is usually the request/response cycle. A single ASP.NET MVC controller might touch multiple repositories, but ideally it should only involve one ISession. The request/response cycle also often serves as the database transaction boundary.

Though some ASP.NET/WCF applications may hold a unit of work open for longer than a single request/response, it's generally agreed that it is a bad idea. Request/response is almost always the perfect model.

Unit of work in rich clients

Rich client applications don't tend to be so simple. Often, the lifetime of your unit of work depends on the user experience you're implementing. Here are some examples from applications I've worked on.

Short - Unit of Work per Interaction

In this search screen, my unit of work can be very small. When the search button is clicked, I open an ISession, fetch some results, and close the session immediately. The unit of work should only be open for a few milliseconds to a second.

A search screen

This unit of work model is usually the easiest to implement, though it can mean you can't lean on the ISession for change tracking and other useful features.

Medium - Unit of Work per View

In this bulk edit screen, I might open an ISession, fetch some results, and keep the session open while the user edits the changes. When they click Save, I'll commit the ISession. My unit of work might be open for a few seconds to a few minutes.

Bulk edit

This unit of work model is tricker to implement, since you need to have well defined "close" points. If your views are Windows/Dialogs, that's easy. If you're using WPF pages, it's harder, since the page can remain in the back/forward stack for some time. You need to think hard about when the right time to close each unit of work is.

Long - Unit of Work per Workflow

In this wizard UI, I might build a unit of work that stays open for the entire business process, growing larger and larger as the user makes their way through the various screens.

A navigation centric wizard

This unit of work model is probably the hardest to manage, since you need to find a nice way to share the unit of work between views/view models. If you only have one workflow active, making your unit of work a singleton could work. But if you have multiple workflows active at once, each with their own unit of work, it's harder.

Unit of work in Magellan

Magellan, like ASP.NET MVC, encourages you to use the "short" unit of work approach by default. For example:

public class SearchController : Controller
{ 
    public ISession Session { get; set; }

    public ActionResult Search(string text)
    {
        var results = Session.QueryOver<Customer>()
            .Where(Restrictions.Like("Name", text))
            .List();

        return Page(new SearchViewModel(results));
    }
}

When a Controller is resolved, its dependencies, including an ISession, might be injected. The controller might use the session to fetch some information, populate a view model, and render a view. By the time the view appears on screen, the Controller and its dependencies, including the ISession, will have been disposed.

Generally, I've found WPF applications are much easier to implement when the unit of work is scoped to a single request.

You can make Magellan support the other two unit of work models I've described above, by making a few hacks in a custom IControllerFactory, but I'd encourage you to consider the short unit of work model.

The short unit of work model also works well if you intend to someday switch your application to go through a service layer.

Why MVC in WPF makes sense

When you use MVVM by itself, the boundary around a unit of work is never clear, and might be inconsistent from view to view.

In Magellan, you use a combination of a MVVM and MVC. The controller formalizes the scoping a request - it's perfect for navigation and invoking external services. We still use a ViewModel, but it only deals with state and behavior - integration and unit of work is managed by the controller.

The key point of this post is that unit of work in WPF applications can be more complicated than web applications. I personally find the separation that MVC encourages is a great way to make me think hard about how units of work are managed, while implementing it in a consistent way.

A picture of me

Welcome, my name is Paul Stovell. I live in Brisbane and work on Octopus Deploy, an automated deployment tool for .NET applications.

Prior to founding Octopus Deploy, I worked for an investment bank in London building WPF applications, and before that I worked for Readify, an Australian .NET consulting firm. I also worked on a number of open source projects and was an active user group presenter. I was a Microsoft MVP for WPF from 2006 to 2013.

16 Feb 2011

Magellan/MVC has changed the way I think about implementing rich client apps - after seeing it in action it now seems obvious to me that Controllers are the puzzle piece that most rich clients are missing.

Taking the 'transactional' logic out of View Models also makes them much more focused in their responsibilities.

I'd love to hear more about your experiences with this pattern in different situations - I'm really enjoying learning some more disciplined approaches to lifetime in rich client apps.

16 Feb 2011

Generally I use IStatelessSession instead of ISession if all I am doing is loading objects for a search/display and don't need any state change tracking.

17 Feb 2011

I like the way you describe that the Controller.Action formalizes the boundary of the unit of work to be done. So Controller.Action == UnitOfWork.DoWork.

I think that there needs to be some consideration as to how the state of ISession is scoped. For example, what implications might arise if I pass ISession to a child component. And even further, what if that child component is then cached?

17 Feb 2011

I should clarify my previous comment... you say pretty clearly that the unit of work is not Controller.Action (that only forms a logical contextual boundary) but the unit of work is more like:

  • var foo = new DbContext
  • foo.Add
  • foo.Add
  • foo.CommitChanges

My comment about the scoping of ISession is still relevant though

17 Feb 2011

Hi Darren,

Generally the ISession shouldn't be passed to other components, but should be injected into them via the container. So for example, suppose my Repository<T> looks like this:

public class Repository<T>
{
    public Repository(ISession session) { .. }
}

My controller could then look like this:

public class SearchController
{
    public IRepository<Customer> Customers { get; set; }
    public IRepository<Order> Order { get; set; }

    // .. actions
}

In my IOC container, I'd register the components with the following lifetimes (using Autofac terminology):

  • ISession: Instance per lifetime scope
  • Controller: Instance per dependency ("transient")
  • IRepository<>: Instance per dependency

My ControllerFactory would create a lifetime scope per controller it resolves, so that every repository in the controller gets the same ISession.

If another component needed access to the session within the controller action, then that component would take a dependency on ISession and it would be resolved from the container just like repositories do. They're all resolved within the same lifetime scope so they'll share the same session.

Paul

17 Feb 2011

Thanks for the response. I think extending the discussion about lifetimes would be good.

20 Feb 2011

What prototyping tool you have used? Found some new done with Silverlight, you might take a look www.mockupbuilder.com

Martin
Martin
21 Feb 2011

"Generally the ISession shouldn't be passed to other components, but should be injected into them via the container."

I don't like this sort of dogmatic thinking. It leads to overly complex designs and fails to utilize the power of the ISession/Datacontext.

For example, if you have an OrderViewModel and OrderItemViewModel, the most elegant way is to have the repository inject the ISession/Datacontext into the OrderViewModel which in turn injects the ISession/DataContext into the OrderItemViewModel.

The OrderViewModel then takes care of saving / transactions for the entire order and there is no need to co-ordinate anything as it will be implicitly handled by the ISession.

21 Feb 2011

Hi Martin,

I agree that not everything fits neatly into that basket. In the cases where it doesn't the rule would be:

  • If you're passing the unit of work to another component, the component shouldn't live longer than the unit of work.

Which I suppose is a good rule to follow when it comes to managing any object lifetime.

Now, I could be nit-picking your example but:

  • Could you resolve the OrderItemViewModel from the container too?
  • If not, does the OrderItemViewModel really need the full ISession? Wouldn't it just wrap a real OrderItem?
  • By having the OrderViewModel pass depenencies to the OrderItemViewModel, aren't you doing the job of the IOC container?

I think a lot of it depends on your overall application architecture. In a Magellan application, an ISession would never be passed to a ViewModel in the first place.

Paul

Martin
Martin
22 Feb 2011

I agree with your first rule, that the child (OrderItemViewModel) should not outlive the unit of work.

Could you resolve the OrderItemViewModel from the container too?

It follows that the OrderItemViewModel would not be resolved from the container, as it probably has no purpose as a standalone (eg it doesn't know how to fetch itself, save itself....)

If not, does the OrderItemViewModel really need the full ISession? Wouldn't it just wrap a real OrderItem?

It might not be that common for the OrderItemViewModel to need the full ISession, however there are certainly cases where it makes for a cleaner implementation. For example, lets say an OrderItem has a property called DeliveryMethod which is a lookup table in the database. I prefer the OrderItemViewModel to populate a collection (for the view to bind to) of DeliveryMethods from the ISession rather than require the OrderViewModel to pass them in. There are many other examples where the OrderItemViewModel might need other adhoc stuff from the ISession.

By having the OrderViewModel pass depenencies to the OrderItemViewModel, aren't you doing the job of the IOC container?

I have a different philosophy here. Injecting dependencies via constructors has been sound software engineering long before IOC containers existed, therefore I don't think everything that gets injected into a constructor has to come from the IOC container. Particularly in this case the ISession is created by the IOC, but just propogated from parent to child.

The whole point of my argument is to keep it simple. There is no need to co-ordinate the units of work, as it happens implicitly. However, you still get the benefit of encapsulating as much of the OrderItemViewModel related logic in its own class.

To be honest, I haven't looked at Magellan (yet), I was just discussing the general point. I am interested to see how you separate the VMs from an ISession (without creating a lot of work :-)