Generic Repositories Part Deux

In yesterday's post, I described a couple of generic repository patterns and explained why I didn't really "get" the concept and preferred a non-generic approach.

I'm happy to say that after a lot of discussion, both on Twitter and in the comments of the post, I'm starting to change my mind.

My primary concern was that simply depending on an IRepository in your business logic didn't seem "explicit" enough, in that it essentially gave the class full access to the database. This tweet from Jak Charlton made me rethink this notion:

@mabster a Command can access whatever it likes, that is its responsibility - why limit with artificial constraints?

So with that in mind, the idea of a single generic repository "per session" starts to make sense.

The Spike

I have a non-trivial domain model here at work that I've been spiking to see how easy it is to work with Entity Framework in a testable fashion. Ripping out all my non-generic repositories and replacing them with a single interface has cut out a lot of code, which IMHO is a good thing.

Here's my interface:

public interface IRepository 
    IEnumerable<TEntity> Find<TEntity>(IQuery<TEntity> query, params string[] includeProperties) where TEntity : class; 

    TEntity Add<TEntity>(TEntity entity) where TEntity : class; 
    void Remove<TEntity>(TEntity entity) where TEntity : class;

    int SaveChanges();

... and the IQuery<T> interface it uses:

public interface IQuery<T> where T : class
    IEnumerable<T> Search(IQueryable<T> items);

I then implement that directly on my DbContext derived class, like this:

public IEnumerable<TEntity> Find<TEntity>(IQuery<TEntity> query, params string[] includeProperties) where TEntity : class
    var set = includeProperties.Aggregate((IQueryable<TEntity>)Set<TEntity>(), (current, includeProperty) => current.Include(includeProperty)); 

    return query.Search(set);

public TEntity Add<TEntity>(TEntity entity) where TEntity : class
    return Set<TEntity>().Add(entity);

public void Remove<TEntity>(TEntity entity) where TEntity : class

You'll note that my "Find" method takes a list of strings representing the properties to eager-load, so I can say repo.Find(new FarmDateQuery(farm, date), "Farm") and get back a list of events with the Farm property loaded. I could have done this with a list of Expression<TEntity, Object> so it's strongly typed, but I haven't found a way to test that the expressions passed to the method are the correct ones. Comparing strings is easy; comparing expressions not so much. Props to Ciaran O'Neill for the idea of using LINQ's Aggregate method to roll up the includes.

So now I just have one repository interface, implemented on my DbContext, and a bunch of "service" classes which contain the business logic and talk to the database via the interface. Everything's testable and I have unit tests that really do explain the business logic rather than the plumbing.

One last note: A few people have suggested not using a repository abstraction at all, and instead talking directly to the DbContext. To those people: I'm curious how you unit test this. Do you really go through the pain of mocking IQueryable<T> and all the LINQ expressions that might be called on it? Or do you just not bother? Let me know in the comments!

data entity-framework c# .net
Posted by: Matt Hamilton
Last revised: 17 Jun, 2024 01:43 AM History



31 Jul, 2012 12:11 AM @ version 1

This is very similar to the model I have been using over the last year, but we use an Expression> for the query and have separate parameters for sorting and paging.

Using the IQuery (is this IQueryable or something outside of EF?) requires a dependency in your business layer which we avoided with the use of the Expression.

It works very well for testing, and provides most of the flexibility of the ORM without too much overhead. We found there were still some instances where we needed a concrete repository (for some complex queries and calculations), but 95% of our data access was via the generic repository.

31 Jul, 2012 12:21 AM @ version 1

IQuery<T> is just an interface I've designed for use by the repository's Find method, Jason.

I'm trying to follow the open/closed principle here, in that I want an easy way to query my database from the business logic layer in ways that I haven't thought of yet, without having to go back and change the repository with every new type of query.

31 Jul, 2012 12:39 AM @ version 1

Right, then that looks to be a nicer solution in that you can extend your IQuery to support paging, sorting and whatever else without having to include this in the repository itself.

Cheers for the clarification.

Colin Scott
Colin Scott
31 Jul, 2012 01:40 AM @ version 1

There are two alternatives I've used in testing without this kind of abstraction. You can call AsQueryable on a collection populated with test data. Alternately use can test against an in memory database. I tend to prefer the later. I'm not sure if Entity Framework supports this (and "my ORM is better than your ORM" arguments tend to be unhelpful) so I'm not sure if this would work for you.

I also tend to prefer architectural separation between the code that queries and the business logic so when testing the business logic you can new up the things you need. This would make integration tests for your queries more plausible against an external DB (although you take a performance and maintainability hit from doing so).

Don Tomato
Don Tomato
06 Aug, 2012 06:00 AM @ version 1

Hi Mat

When i try to click on 'C#' tag, I get the error (see below) I use Google Chrome (but checked it in the Firefox also) Apparently, the blog engine may not work properly with the # symbol.

Server Error in '/' Application.

Sequence contains no elements

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 

Exception Details: System.InvalidOperationException: Sequence contains no elements
06 Aug, 2012 07:38 AM @ version 1

G'day Don,

Yeah, I'm running a pretty old (like, over a year old) build of FunnelWeb. I'm sure this has been fixed in a later version. I just have to muscle up the gumption to upgrade my site. :)

No new comments are allowed on this post.