Share via


Creating a repository with a fluent interface

This article details how to implement a fluent interface on a repository in order to provide a flexible query API without having to return IQueryable, and the merits of this approach compared to more common alternatives. A code sample to accompany this article can be downloaded here

The problem: whether to return IQueryable<T> from a repository

There is a lot of debate regarding the best way to implement a repository when using Entity Framework much of which centres on whether to return IQueryable<T> from the repository or not. When working directly with a DbContext the user has the ability to create and extend queries by chaining extension methods as in the example below.

var results = context.Authors
                .Include(a => a.Books)
                .Where(a => a.Name != "Orwell")
                .OrderBy(a => a.Name)
                .ToList();

This is possible due to the use of IQueryable<T> which will only be executed when either when the query results are iterated. IQueryable makes the querying in Entity Framework both very flexible and readable. 

When wrapping a DbContext in a repository it would be nice to provide a similar functionality. The simplest way to do this is to return IQueryable<T> from the repository as in the code snippet below.

public class  Repository : IRepository
{
    private DbContext context;
 
    public Repository(DbContext context)
    {
        this.context = context;
    }
 
    public IQueryable<T> Query<T>() where T : class
    {
        return this.context.Set<T>().AsQueryable<T>();
    }     
}

The consumer can then query the repository in the same way as if they were using the context directly.

Repository rep = new  Repository(context);
                        
var results = rep.Query<Author>()
        .Include(a => a.Books)
        .Where(a => a.Name != "Orwell")
        .OrderBy(a => a.Name)
        .ToList();

However doing so has some drawbacks which lessen the benefits of using the repository pattern in the first place. These include:

  • The code consuming the repository becoming polluted with dependencies on the Entity Framework. To use the queryable extensions the client code is going to need to reference Entity Framework resulting in the abstraction between the data layer and the consuming layer has being lost.
  • Loss of the ability to test the repository (as you cannot control how the returned IQueryable<T> will be used).
  • Loss of the ability to implement effective error handling. As the execution is deferred any exceptions thrown when a query is executed will be outside of the repository.

So the alternative is to provide a repository that allows for the different usage requirements of the consumer through a combination of multiple query methods and overloads of those methods. This approach can get very difficult both in terms of managing the code and working with the finished repository. See below for an example of a repository starting to get out of control.

public interface  IRepository: IDisposable
{
    IEnumerable<T> Query<T>(Expression<Func<T, bool>> predicate) where T : class;
    IEnumerable<T> Query<T>(Expression<Func<T, bool>> predicate, string[] include) where T : class;
    IEnumerable<T> Query<T>(Expression<Func<T, bool>> predicate, string[] include, int  page, int  pageSize) where T : class;
    IEnumerable<T> Query<T>(Expression<Func<T, bool>> predicate, int  page, int  pageSize, string[] sortBy, bool  ascending) where T : class;
    IEnumerable<T> Query<T>(Expression<Func<T, bool>> predicate, string[] include, int  page, int  pageSize, string[] sortBy, bool  ascending) where T : class;
}

An alternate approach: using a fluent interface

There is another option that provides much more flexibility and usability to the user without returning IQueryable<T>, which is to create a fluent interface for the repository. It will allow for the familiar usage of chaining methods but at the same time avoid  the aforementioned negatives. A sample usage of the repository is shown below.

var fluentRepository = new  FluentRepository(new  SampleContext());
 
fluentRepository.Query<Author>()
     .Where(a => a.Name != "Orwell")
     .Include(a => a.Books)
     .OrderBy(a => a.Name)
     .ToList();

Creating a 'fluent' repository

This is achieved by having the repository return an interface which defines the methods the consumer may use to modify the initial query (Where, Include, OrderBy, OrderByDescending, Page and any others that are required). These methods all return the same interface which allows for the methods to be chained together.

Another set of methods is also defined by the interface. These will cause the execution of the query and return the actual results of the query.

The repository just has to return a concrete implementation of this interface as the result of a query called on it.

public interface  IFluentRepository
{
    IQueryBuilder<T> Query<T>() where T : class;
}

​IQueryBuilder<T> is the fluent interface.

public interface  IQueryBuilder<T> where T : class
{
    IQueryBuilder<T> Where(Expression<Func<T, bool>> predicate);
    IQueryBuilder<T> Include(Expression<Func<T, object>> path);
    IQueryBuilder<T> OrderBy(Expression<Func<T, object>> path);
    IQueryBuilder<T> OrderByDescending(Expression<Func<T, object>> path);
    IQueryBuilder<T> Page(int page, int pageSize);
 
    T FirstOrDefault();
    Task<T> FirstOrDefaultAsync();
 
    List<T> ToList();
    Task<List<T>> ToListAsync();
 
    int Count();
    Task<int> CountAsync();
}

​The concrete implementation is quite straightforward. For the query 'manipulation' methods it simply holds the query, modifies it according to the method called and then returns itself again as the result. For the methods which call for results from the query it executes the current query and returns them.

public class  QueryBuilder<T> : IQueryBuilder<T> where T : class
{
    private DbContext context;
    private IQueryable<T> query;
 
    public QueryBuilder(DbContext context)
    {
        this.context = context;
        this.query = this.context.Set<T>();
    }
 
    public IQueryBuilder<T> Where(Expression<Func<T, bool>> predicate)
    {
        this.query = this.query.Where(predicate);
        return this;
    }
 
    public IQueryBuilder<T> Include(Expression<Func<T, object>> path)
    {
        this.query = this.query.Include(path);
        return this;
    }
 
    public IQueryBuilder<T> OrderBy(Expression<Func<T, object>> path)
    {
        this.query = this.query.OrderBy(path);
        return this;
    }
 
    public IQueryBuilder<T> OrderByDescending(Expression<Func<T, object>> path)
    {
        this.query = this.query.OrderByDescending(path);
        return this;
    }
         
    public IQueryBuilder<T> Page(int page, int pageSize)
    {
        this.query = this.query.Skip(page * pageSize).Take(pageSize);
        return this;
    }
 
    public T FirstOrDefault()
    {
        return this.query.FirstOrDefault<T>();
    }
 
    public Task<T> FirstOrDefaultAsync()
    {
        return this.query.FirstOrDefaultAsync();
    }
 
    public List<T> ToList()
    {
        return this.query.ToList();
    }
 
    public Task<List<T>> ToListAsync()
    {
        return this.query.ToListAsync();
    }
 
    public int  Count()
    {
        return this.query.Count();
    }
 
    public Task<int> CountAsync()
    {
        return this.query.CountAsync();
    }
}

Finally the repository returns a new instance of the QueryBuilder<T> fluent interface when Query<T> is called.

public class  FluentRepository : IFluentRepository
{
    private DbContext Context { get; set; }
 
    public FluentRepository(DbContext context)
    {
        this.Context = context;
    }
 
    public IQueryBuilder<T> Query<T>() where T : class
    {
        return new  QueryBuilder<T>(this.Context);
    }
}