.NET & Funky Fresh

Syndication

News

  • <script type="text/javascript" src="http://ws.amazon.com/widgets/q?ServiceVersion=20070822&amp;MarketPlace=US&amp;ID=V20070822/US/bluspiconinc-20/8001/8b68bf4b-6724-40e7-99a5-a6decf6d8648"> </script>
Images in this post missing? We recently lost them in a site migration. We're working to restore these as you read this. Should you need an image in an emergency, please contact us at imagehelp@codebetter.com
My NHibernate 2.0 Supa' Layer

From the time that I started learning about C# 3.0 and LINQ I've had a number of interesting ideas spinning about in my head.  Last week I finally got some free time to work them out.  I'm pretty excited about the results.

Essentially I wanted a way to make queries work better within the context of a Domain Driven Design approach.  I'm a big believer in Persistence Ignorance, so this means that I had to find a way to represent queries in a provider independent way.   LINQ does this for me, mostly, but I still need a 'syntactically pleasing' way to get the LINQ provider to the query at runtime.  Well, it turns out that doing this is pretty easy.  Once I figured this out and got a basic implementation working, I realized more and more ways to improve the readability of the code through a little DSL invention.  I'll discuss the key parts of the solution below.

WARNING: This code has not been used in a production environment yet.  Use at your own risk.

Let's first look at the generic repository definition:

public interface IRepository<T>
{
    T Get(object key);

    IQueryable<T> Find(Func<IQueryable<T>, IQueryable<T>> queryableTransformer);
    IPage<T> Paginate(IQueryable<T> query, int pageNumber, int pageSize);

    void Save(T entity);
    void Delete(T entity);
}

Notice the delegate signature used by the Find method.   This method is used by the repository to provide the NHibernate.Linq provider at runtime.  Here's how you would use this:

var results = Repository<Customer>.Find(
    customers => from c in customers
                 where c.FirstName == "Rob"
                 select c
    );

Now, this syntax works, but it doesn't give me everything that I am looking for.  I'd like to have a way to encapsulate and reuse my queries.  This would help keep query logic inside the domain model and not spread throughout the application logic.  Additionally, I'd like a way run these same queries against in-memory data without any alterations.  Finally, I'd like a more human readable syntax like this:

var results = Find.Customers.WithFirstName("Rob");
var pagedResults = Find.Customers.ThatAreActive.Page(3);

//the following queries execute in a single batch
var futureResults1 = Find.Customers.WithLastName("Eisenberg").AsFuture.Count();
var futureResults2 = Find.All<Customer>().AsFuture.Page(4, 20);

So, how do I get get there?  The first step was to create a class to encapsulate that funky delegate.  Here's what I came up with:

public class Query<T> : IQueryable<T>
{
    private readonly Func<IQueryable<T>, IQueryable<T>> _transformer;
    private IEnumerable<T> _source;
    private IQueryable<T> _queryable;

    public Query(Func<IQueryable<T>, IQueryable<T>> transformer)
    {
        _transformer = transformer;
    }

    public FutureCapable<T> AsFuture
    {
        get { return new FutureCapable<T>(Transformer); }
    }

    public Func<IQueryable<T>, IQueryable<T>> Transformer
    {
        get { return _transformer; }
    }

    public void SetSource(IEnumerable<T> enumerable)
    {
        _source = enumerable;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public IEnumerator<T> GetEnumerator()
    {
        TryExecute();
        return _queryable.GetEnumerator();
    }

    public Expression Expression
    {
        get
        {
            TryExecute();
            return _queryable.Expression;
        }
    }

    public Type ElementType
    {
        get
        {
            TryExecute();
            return _queryable.ElementType;
        }
    }

    public IQueryProvider Provider
    {
        get
        {
            TryExecute();
            return _queryable.Provider;
        }
    }

    private void TryExecute()
    {
        if(_queryable != null) return;
        _queryable = _source != null ? _transformer(_source.AsQueryable()) : Repository<T>.Find(_transformer);
    }
}

This encapsulation of the query implements the IQueryable interface by passing through to the lazily evaluated inner IQueryable.  I have provided a SetSource method, allowing the developer to run the query over any in memory IEnumerable.  If the source is not set, it will default to a repository.  The next step is to define some queries in my domain model.

Here is the code from Find.cs

public static partial class Find
{
    public static Query<T> All<T>()
    {
        return new Query<T>(
            source => from item in source
                      select item
            );
    }
}

And here is the code from Customers.cs

public static partial class Find
{
    public static class Customers
    {
        public static Query<Customer> ThatAreActive
        {
            get
            {
                return new Query<Customer>(
                    customers => from c in customers
                                 where c.IsActive
                                 select c
                    );
            }
        }

        public static Query<Customer> WithFirstName(string firstName)
        {
            return new Query<Customer>(
                customers => from c in customers
                             where c.FirstName == firstName
                             select c
                );
        }

        public static Query<Customer> WithLastName(string lastName)
        {
            return new Query<Customer>(
                customers => from c in customers
                             where c.LastName == lastName
                             select c
                );
        }
    }
}

Ok, this code gets me to this:

var results = Find.Customers.WithFirstName("Rob");

foreach(var customer in results)
{
    Console.WriteLine("{0} {1} ({2})", customer.FirstName, customer.LastName, customer.ID);
}

To get pagination, we add a few extension methods:

public static class PageExtensions
{
    public static int DefaultPageSize = 20;

    public static IPage<T> Page<T>(Func<IQueryable<T>, IQueryable<T>> queryableTransformer, int pageNumber, int pageSize)
    {
        return Repository<T>.Paginate(queryableTransformer, pageNumber, pageSize);
    }

    public static IPage<T> Page<T>(Func<IQueryable<T>, IQueryable<T>> queryableTransformer, int pageNumber)
    {
        return Repository<T>.Paginate(queryableTransformer, pageNumber, DefaultPageSize);
    }

    public static IPage<T> Page<T>(this Query<T> query, int pageNumber, int pageSize)
    {
        var queryable = Repository<T>.Find(query.Transformer);
        return Repository<T>.Paginate(queryable, pageNumber, pageSize);
    }

    public static IPage<T> Page<T>(this Query<T> query, int pageNumber)
    {
        var queryable = Repository<T>.Find(query.Transformer);
        return Repository<T>.Paginate(queryable, pageNumber, DefaultPageSize);
    }

    public static IPage<T> Future<T>(this IPage<T> page)
    {
        return DI.Resolve<IFutureProvider>().Page(page);
    }
}

The Paginate method of the repository returns a custom IPage implementation. Here's what that implemntation looks like for NHibenrate:

public class NHPage<T> : IPage<T>, IQueryProvider
{
    private readonly ISession _session;
    private readonly int _pageNumber;
    private readonly int _pageSize;
    private readonly IQueryable<T> _selection;

    private int _itemCount;
    private IList _selectionResult;

    public NHPage(ISession session, IQueryable<T> queryable, int pageNumber, int pageSize)
    {
        _session = session;
        _pageNumber = pageNumber;
        _pageSize = pageSize;

        _selection = queryable;
    }

    public IQueryable<T> Selection
    {
        get { return _selection; }
    }

    public int PageNumber
    {
        get { return _pageNumber; }
    }

    public int PageSize
    {
        get { return _pageSize; }
    }

    public int ItemCount
    {
        get
        {
            TryExecuteQuery();
            return _itemCount;
        }
    }

    public int PageCount
    {
        get { return (int)Math.Ceiling(((double)ItemCount) / PageSize); }
    }

    public Expression Expression
    {
        get { return _selection.Expression; }
    }

    public Type ElementType
    {
        get { return _selection.ElementType; }
    }

    public IQueryProvider Provider
    {
        get { return this; }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public IEnumerator<T> GetEnumerator()
    {
        TryExecuteQuery();

        foreach (var item in _selectionResult)
        {
            yield return (T)item;
        }
    }

    private void TryExecuteQuery()
    {
        if (_selectionResult != null) return;

        ICriteria selectionCriteria = GetCriteria(_selection);
        selectionCriteria.SetMaxResults(_pageSize).SetFirstResult(_pageSize * (_pageNumber - 1));

        ICriteria countCriteria = CriteriaTransformer.TransformToRowCount(selectionCriteria);
        
        IList multiResult = _session.CreateMultiCriteria()
            .Add(selectionCriteria)
            .Add(countCriteria)
            .List();

        _selectionResult = (IList) multiResult[0];
        _itemCount = (int) ((IList) multiResult[1])[0];
    }

    private static ICriteria GetCriteria(IQueryable<T> query)
    {
        PropertyInfo _rootCriteriaProperty = typeof (NHibernateLinqQuery<T>)
            .GetProperty("RootCriteria", BindingFlags.Instance | BindingFlags.NonPublic);

        return (ICriteria) _rootCriteriaProperty.GetValue(query, null);
    }

    public IQueryable CreateQuery(Expression expression)
    {
        var queryable = _selection.Provider.CreateQuery(expression);
        var elementType = LinqUtil.GetElementType(expression);
        return (IQueryable)Activator.CreateInstance(typeof(NHPage<>).MakeGenericType(elementType), _session, queryable, _pageNumber, _pageSize);
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        var queryable = _selection.Provider.CreateQuery<TElement>(expression);
        return new NHPage<TElement>(_session, queryable, _pageNumber, _pageSize);
    }

    public object Execute(Expression expression)
    {
        throw new NotSupportedException();
    }

    public TResult Execute<TResult>(Expression expression)
    {
        throw new NotSupportedException();
    }
}

NOTE: The RootCriteria property of NHibernateLinqQuery is not currently public, so we have to use reflection to get it.

And here's how the NHibernate repository uses it:

public class NHRepository<T> : IRepository<T>
{
    public ISession Session
    {
        get { return NH.CurrentSession; }
    }

    public T Get(object key)
    {
        return Session.Get<T>(key);
    }

    public IQueryable<T> Find(Func<IQueryable<T>, IQueryable<T>> queryableTransformer)
    {
        return queryableTransformer(new NHibernateLinqQuery<T>(Session));
    }

    public IPage<T> Paginate(IQueryable<T> queryable, int pageNumber, int pageSize)
    {
        var future = queryable as NHFutureQueryable<T>;

        if (future != null) return new NHFuturePage<T>(future.ID, future.Queryable, pageNumber, pageSize);
        return new NHPage<T>(Session, queryable, pageNumber, pageSize);
    }

    public void Save(T entity)
    {
        Session.Save(entity);
    }

    public void Delete(T entity)
    {
        Session.Delete(entity);
    }
}

This gets us everything but futures. But with a few more interfaces (IFutureProvider, IFuture) and several NHibernate implementations, we can get there without too much hastle.  Please download the code and try it out.  Let me know what you think.  There are some obvious features missing (particularly on IFutureProvider).  I have borrowed a few low level pieces from Ayende's Rhino.Commons to whom I am very grateful.  His work and several important blog posts are what got my brain ticking to begin with.

ASIDE:  There is also a very basic implementation of an NHibernate mapping DSL in the solution.  I'd love to know if anyone else is interested in this.  If so, I'll spend some more time fleshing it out.

Download the Source.


Posted 04-03-2008 11:31 AM by Rob Eisenberg
Filed under: , , , ,

[Advertisement]

Comments

Vladan Strigo wrote re: My NHibernate 2.0 Supa' Layer
on 04-03-2008 1:05 PM
This looks really great! I am currently working on a small glue framework for which a big part of is integration with NHibernate and is largely influenced by Ayende's great work. Point being... I've written a few weeks ago a dsl implementation for FInd based on a Fluent interface and now that NH 2.0 Alpha came out I decided to drop it and start using Ayende's Linq for NH with it implemented via Repository Why I say this is sooo great... It is because it proves to me that my ideas can actually work (even gives me some ideas from where to start). Hope you continue your work and that you don't mind if I take a peek from time to time ;) Cheers! Vladan Strigo
DotNetKicks.com wrote NHibernate 2.0 Supa' Layer
on 04-03-2008 3:01 PM

You've been kicked (a good thing) - Trackback from DotNetKicks.com

Steve Gentile wrote re: My NHibernate 2.0 Supa' Layer
on 04-03-2008 3:30 PM
Good write up. I'd like to take something like this and use it in a web application. I'd like to see how you handle the sessions with this in that context.
Reflective Perspective - Chris Alcock » The Morning Brew #66 wrote Reflective Perspective - Chris Alcock &raquo; The Morning Brew #66
on 04-04-2008 3:29 AM

Pingback from  Reflective Perspective - Chris Alcock  &raquo; The Morning Brew #66

Rob Eisenberg wrote re: My NHibernate 2.0 Supa' Layer
on 04-04-2008 8:19 AM

@Steve

I'll definitely be doing some more posting on this.  I'll certainly show how to use this in a web app.  Before I do that I need to add a few more pieces, but when it's done it should be very clean.

Dew Drop - April 4, 2008 | Alvin Ashcraft's Morning Dew wrote Dew Drop - April 4, 2008 | Alvin Ashcraft's Morning Dew
on 04-04-2008 9:08 AM

Pingback from  Dew Drop - April 4, 2008 | Alvin Ashcraft's Morning Dew

Links Today (2008-04-04) wrote Links Today (2008-04-04)
on 04-04-2008 11:33 AM

Pingback from  Links Today (2008-04-04)

Sean Chambers wrote re: My NHibernate 2.0 Supa' Layer
on 04-06-2008 11:44 AM
Really interesting use of Linq and NHibernate to create fluent interfaces with querying. Most inventive use I've seen so far. However, while neat from an architecture standpoint, I feel this is alot of extra code for very little benefit. It is nice to type out Find.Customers.WithFirstName, but in the long run this could become a maintenance nightmare. In addition from a DDD perspective, this slightly blurs your aggregate roots and makes it a little more obscure. Although from a non-DDD context my thoughts are irrelevant. Don't get me wrong though, this is a very cool usage of Linq with fluent querying interfaces. good work!
Rob Eisenberg wrote re: My NHibernate 2.0 Supa' Layer
on 04-06-2008 2:44 PM

Thanks for the feedback Sean.  Could you expound a little more about what/how you feel the the aggregates could be blurred?  I'm a big believer in DDD, but struggle often to find the most 'DDD way' to express an idea - especially around designing and enforcing aggregate boundaries.  So, I'd greatly appreciate some additional feedback.  As to the amount of code that it takes to implement the solution, I don't think it's really that much.  After all this:

public static Query<Customer> WithLastName(string lastName)  

{  

   return new Query<Customer>(  

       customers => from c in customers  

                             where c.LastName == lastName  

                             select c  

       );  

}

isn't that much code to create a query.  On the other hand, I do agree that the query situation could get out of hand and become a maintenance nightmare if not constantly refactored.  Do you have any ideas/recommendations as to how to keep repositories/queries in the domain model in a highly maintainable state?  I've yet to hear anyone specifically address this issues in a book or blog and I'd love to see how others are managing it.

Ryan Cromwell wrote re: My NHibernate 2.0 Supa' Layer
on 04-16-2008 10:54 AM
Your Query and partial Find classes look awefully similar to NHibernate Query Generator output.
Rob Eisenberg wrote re: My NHibernate 2.0 Supa' Layer
on 04-16-2008 12:25 PM

@Ryan

NHibernate Query Generator is more of a replacement for LINQ than what I'm doing.  (Or should I say, it was a pre-LINQ solution that provided intellisense and allowed devs to avoid using the NHibernate ICriteria system directly).  What I'm trying to do is better encapsulate the actual queries and provide a simple DSL from the perspective of the domain model.

So you want to learn NHibernate? - Part 1 of 1, The Links « HSI Developer Blog wrote So you want to learn NHibernate? - Part 1 of 1, The Links &laquo; HSI Developer Blog
on 07-31-2008 6:27 PM

Pingback from  So you want to learn NHibernate? - Part 1 of 1, The Links &laquo; HSI Developer Blog

So you want to learn NHibernate? (or, NHibernate Hyperlink Acupuncture) | The Freak Parade wrote So you want to learn NHibernate? (or, NHibernate Hyperlink Acupuncture) | The Freak Parade
on 08-08-2008 3:28 PM

Pingback from  So you want to learn NHibernate? (or, NHibernate Hyperlink Acupuncture) | The Freak Parade

DotNetShoutout wrote NHibernate 2.0 Supa' Layer
on 11-19-2008 5:50 PM

Your Story is Submitted - Trackback from DotNetShoutout

Code Monkey Labs wrote Weekly Web Nuggets #6
on 02-22-2009 10:51 PM

General Silverlight String.Format Tool : Ever struggled with getting dates, times, or numbers to format just how you like? No more! Check out this neat tool using Silverlight 2 that lets you select your way to a format string. The Past, Present and Future

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)