.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]

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)