Monday, September 20, 2010

Silverlight Cookbook: WCF Data Services and NHibernate

This post is part of a series of blog posts detailing various aspects of the Silverlight Cookbook, an initiative to demonstrate proper design choices for building enterprise-class line-of-business applications with Silverlight (and WPF if you will). It currently consists out of the following parts.

In the Silverlight Cookbook I use a variation of the CQRS pattern where business-oriented commands are used to create, update or change the data, while WCF Data Services (formally ADO.NET Data Services) is used as a general-purpose query service.

Although WCF Data Services has been designed with the Entity Framework in mind, my desire to use an advanced ORM with features like 2nd level caching has let me to Shawn Wildermuth's attempt to support NHibernate as a WCF Data Service provider. Unfortunately, that solution no longer works with NHibernate 3.x, but I managed to get it working again by compiling my own version. But regardless of all the goodness of NHibernate 3, not everything went as smooth as I hoped for.

For starters, you need to expose all entities as entity sets even though you use them only in associations with another parent entity and never query on them. Consider for instance that we have a Recipe entity that has an aggregate relationship with one or more Rating entities. We'll never query directly on the ratings, but we still need to expose them like this:

    public class RestContext : NHibernateContext 
    { 
        public IQueryable<Recipe> Recipes 
        {
             get { return Session.Query<Recipe>();  }
        }

        public IQueryable<Rating> Ratings
        {
            get { return Session.Query<Rating>(); }
        }
    }

Another thing is that WCF Data Services chokes on the proxies that NHibernate generates at run-time. So if your entity lazy-loads another associated entity, you have to disable lazy-loading altogether. I've actually spent quite a few hours on using Reflector to investigate whether the NHibernateContext or DataService classes could be tweaked to properly map the derived proxy class back to the appropriate entity type, but failed. As a workaround, you either need to to disable lazy-loading in the mapping, or eagerly fetch the associations using the Fetch extension method introduced in NHibernate 3: 

   
public class RestContext : NHibernateContext
    {
       public IQueryable<Recipe> Recipes
        {
            get
            {
                return Session
                    .Query<Recipe>()
                    .Fetch(r => r.Ratings);
          }
        }
    }

Unfortunately, that introduces another 'feature' of NHibernate. I've never quite understood why, but when NHibernate fetches one-to-many and many-to-many associations, you end up with duplicate records in the final result set. Just appending a Distinct() to the entity set getter such as the one above doesn't help. This has actually cost me a lot of time to work out, because when loading a lot of entities, the result set ended up to be too big for WCF Data Services to transfer. You'll find the solution in a hidden feature of NHibernate.Linq that allows you to add a custom action to an IQueryable expression tree like this:

    queryable.QueryOptions.RegisterCustomAction(
        c => c.SetResultTransformer(new DistinctRootEntityResultTransformer()));

Since I needed to use this in many places and it obscures the intention of my code, I decided to wrap it in an extension method:

    public static class NHibernateQueryableExtensions
    {
        public static IQueryable<TEntity> DistinctDeferred<TEntity>(this IQueryable<TEntity> queryable)
        {
            var nHibernateQueryable = queryable as INHibernateQueryable<TEntity>;

            if (nHibernateQueryable != null)
            {
                nHibernateQueryable.QueryOptions.RegisterCustomAction(
                    c => c.SetResultTransformer(new DistinctRootEntityResultTransformer()));
            }

            return queryable;
        }
    }

It can be used just like the other extension methods applicable to IQueryable:

    public IQueryable<Registration> Registrations
    {
        get
        {
            return Session
                .Linq<Registration>()
                .Expand("Product")
                .DistinctDeferred();
        }
    }

Since I don't do any updates through the query service, I haven't tried how well CRUD operations through WCF Data Services work. However, I did notice that in order to receive the latest changes from the server, I have to make sure to set the MergeOption property of the generated DataServiceContext subclass to MergeOption.OverwriteChanges:

    queryContext = new QueryContext(queryServiceUri)
    {
        MergeOption = MergeOption.OverwriteChanges
    };

In the default setting, MergeOption.AppendOnly, it will assume all changes are made by the client, and ignore anything that occurred on the server (beats me why it does that). You can also use MergeOption.NoTracking and get the same behavior, but with MergeOption.OverwriteChanges, two queries with the same key (identified by the [DataServiceKey] attribute) will return the same physical object. This is quite convenient when binding a ComboBox to the list of available entities and the currently selected one.

To conclude this post, let me tell you that debugging WCF Data Services is definitely cumbersome. When something goes wrong or is incorrectly configured you'll only receive a generic error like this: 

clip_image0014 
You can solve that by slapping a [ServiceBehavior] attribute to your data service class like this:

    [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
    public class RestService : DataService<RestContext> { }

Or you can add the corresponding configuration setting to the <behaviors> element of your web.config:

<serviceBehaviors>
    <behavior name="">
        <serviceMetadata httpGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="true" />
    </behavior>
</serviceBehaviors>

Finally, you can force WCF Data Services to include a bit more info on the exception that occurred by changing the UseVerboseErrors property of the DataServiceConfiguration class. 

   public class QueryService : DataService<QueryContext>
    {
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.UseVerboseErrors = true;

The next time you make a mistake or some down-level query operation throws an exception, you end up with something like this:

clip_image0024