Query Objects with the Repository Pattern


Nate Kohari (whose primate brain is far too small to comprehend this post [inside joke, he’s actually really sharp]) was asking on Twitter today about how to structure his repositories: Per aggregate root, Per entity, or just one repository for everything?

The two suggestions that rose to the top were the per-aggregate and one repository approaches. Jeremy and I use a the one repository approach at work and it’s working well for us so far.  The was one compelling argument for the per-aggregate repository and that is that you could encapsulate your aggregate-specific queries inside your aggregate-specific repositories.

Thinking about this a little more, I felt it had a little smell to it, namely the potential for business logic (also known as ‘where’ clauses) to creep into the repository which would be bad. Perhaps a better alternative would be to encapsulate the specific logic of a given query into an object. You could then have this object produce something that the repository could (blindly, decoupled) use to query on.

This approach allows you to maintain the one repository approach, yet still have encapsulated domain-specific queries. Plus, you can test your queries independently of the repository which is a huge benefit.

Creating and Passing Around Expression Trees

What might the query object produce that the repository could blindly use?  In the .NET 3.5 world, you could use expression trees!  These come in quite handy and play well with LINQ-style (IQueryable) behavior of Linq2NHibernate, Linq2Objects, and Linq2JustAboutAnythingThatInvolvesAListOfThings.

Producing an expression tree is actually quite simple:  Have a method or property that returns Expression<Func<T,bool». Yep. That’s it. We can all go home now.  In case you actually wanted to see some code, here’s a simple, extremely naive example of what I mean. Let’s say we wanted to find all our customers that have significant sales with our company, but whose discount is really low (for some reason).  We’d like to find these customers and give them a better discount since they do so much business with us. Your query probably (wouldn’t) look something like this:

public class TopCustomersWithLowDiscountQuery
{
    public bool IncludePreferred { get; set; }
    public decimal DiscountThreshold { get; set; }
    public int SalesThreshold { get; set; }

    public Expression<Func<Customer, bool>> AsExpression()
    {
        return (c =>
                c.Preferred == IncludePreferred
                && c.Discount < DiscountThreshold
                && c.AnnualSales > SalesThreshold);
    }
}

Using this query object, we can still get all the benefits of delayed execution that LINQ/IQueryable provides us without forcing us to sprinkle little surprises of business logic everywhere in LINQ queries.

Using the Expression Tree to Query</p> </p> </p> </p> </p> </p> </p> </p> </p> </p> </p> </p> </p>

To actually use the expression tree, you can call the Where() method on any IEnumerable or IQueryable.  Consider this silly/contrived example:

var query = new TopCustomersWithLowDiscountQuery
{
    IncludePreferred = true,
    DiscountThreshold = 3,
    SalesThreshold = 100000
};

var customerList = new List<Customer>
{
    new Customer {Id = 1, Preferred = true},
    new Customer {Id = 2, Preferred = false},
    new Customer {Id = 3, Preferred = true}
};

var filteredCustomers = 
    customerList
        .AsQueryable()
        .Where(query.AsExpression());

 

Once GetEnumerator() is called on filteredCustomers, the magic happens and you’ll end up with an IEnumerable that has only 2 elements in id (ID=1, and ID=3).</p>

Full Code Example

Here’s the full code of the test fixture I was using for these examples. Yes, I know it’s not very realistic and there are lots of potential problems with the logic in the query object, but the point was to illustrate how you might go about encapsulating LINQ where clauses, so please go easy on me 🙂

[TestFixture]
    public class TopCustomersWithLowDiscountQueryTester
    {
        [Test]
        public void preferred_customers_should_be_selected_when_IncludePreferred_is_true()
        {
            _customers.AddRange(new[]
            {
                new Customer {Id = 1, Preferred = true},
                new Customer {Id = 2, Preferred = false},
                new Customer {Id = 3, Preferred = true}
            });

            Results.Count().ShouldEqual(2);
            Results.ElementAt(0).Id.ShouldEqual(1);
            Results.ElementAt(1).Id.ShouldEqual(3);
        }

        [Test]
        public void high_discount_customers_should_not_be_selected()
        {
            _query.IncludePreferred = false;

            var high = 15m;
            var low = 1m;

            _customers.AddRange(new[]
            {
                new Customer {Id = 1, Discount = high},
                new Customer {Id = 2, Discount = low},
                new Customer {Id = 3, Discount = high}
            });

            Results.Count().ShouldEqual(1);
            Results.ElementAt(0).Id.ShouldEqual(2);
        }

        [SetUp]
        public void SetUp()
        {
            _query = new TopCustomersWithLowDiscountQuery
            {
                IncludePreferred = true,
                DiscountThreshold = 3,
                SalesThreshold = 100000
            };

            _customers = new List<Customer>();
            _resultsCached = null;
        }

        private TopCustomersWithLowDiscountQuery _query;
        private List<Customer> _customers;
        private IEnumerable<Customer> _resultsCached;

        public IEnumerable<Customer> Results
        {
            get { return _resultsCached ?? _customers.AsQueryable().Where(_query.AsExpression()); }
        }
    }

    public static class SpecificationExtensions
    {
        public static void ShouldEqual(this object actual, object expected)
        {
            Assert.AreEqual(actual, expected);
        }
    }

 

Coming up…</p> </p> </p>

Stay tuned: I’m going to do a follow-on post on how you can make the query object code a little more elegant, as well as chain them together with AndAlso and OrElse expressions.

StuctureMap: Advanced-level Usage Scenarios (Part 1: Type/Convention Scanners)