Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Add Fluent API Flavour To Your Repositories

4.00/5 (1 vote)
26 Aug 2015CPOL6 min read 10.9K   78  
Add Fluent API support to your Entity Framework repositories

Introduction

A few days ago, one of my friends asked what’s my thought on using Fluent API for querying a repository. Initially, the idea didn’t appeal to me much but after taking a look at his code, I realized it’s quite an interesting idea. This article is to share how the refactored code provides us a nice extension to repository classes

Background

The article assumes the readers have basic understanding of Repository pattern, Fluent API and Entity Framework. If you need some refresh on Repository pattern and Fluent API, here are some great articles on CodeProject:

Using the Code

In the attached demo solution, we created a very simple console project working with models from Car Rental domain. The model contains only one entity Car with 2 supporting enum CarBrand and CarStatus:

C#
public class Car
{
    public int Id { get; set; }
    public CarBrand Brand { get; set; }
    public string Model { get; set; }
    public decimal RentalPricePerDay { get; set; }
    public CarStatus Status { get; set; }
}

Here are some seed data to initialize the database:

C#
var cars = new List<Car>
{
    new Car { Brand = CarBrand.BMW, Model = "M235i", 
    RentalPricePerDay = 90, Status = CarStatus.Available },
    new Car { Brand = CarBrand.Cadillac, Model = "CTS", 
    RentalPricePerDay = 80, Status = CarStatus.Reserved },
    new Car { Brand = CarBrand.Chevrolet, Model = "Corvette Stingray", 
    RentalPricePerDay = 85, Status = CarStatus.Available },
    new Car { Brand = CarBrand.Ford, Model = "Mustang GT", 
    RentalPricePerDay = 70, Status = CarStatus.Available },
    new Car { Brand = CarBrand.Honda, Model = "Accord", 
    RentalPricePerDay = 60, Status = CarStatus.Rented },
    new Car { Brand = CarBrand.Mazda, Model = "3", 
    RentalPricePerDay = 65, Status = CarStatus.Rented },
    new Car { Brand = CarBrand.BMW, Model = "i8", 
    RentalPricePerDay = 70, Status = CarStatus.Available },
    new Car { Brand = CarBrand.Porsche, Model = "Boxster", 
    RentalPricePerDay = 90, Status = CarStatus.Available }
};

Let’s assume that the client UI needs to display 2 pages which:

  • List all cars that are available with minimum price of 70
  • List all BMW cars that are available with minimum price of 60 and maximum price of 80

In a normal implementation of repository, you will implement 2 methods specific to these 2 use cases in CarRepository (e.g. FindAvailableCarWithMinPrice(), FindAvailableBMWWithPriceWithinRange(), etc). When the project grows larger and larger, you will soon find yourself creating more and more methods that are similar to each other, with just some minor difference between queries (e.g. both queries above involve available car with some min price).

So some repository implementation chooses to expose a general query method using Expression and return IEnumerable like:

C#
public IEnumerable<T> FindBy(Expression<Func<T, bool>> filter)

While it’s flexible and works in most situations, it’s no longer descriptive like the first approach. Now to understand what the query does, developers need to read and understand the lambda filter, and in some complicated cases, it’s not a trivial task.

With the use of Fluent API, we find that it offers a good trade off with above 2 approaches. It’s not silver bullet and cannot be applied blindly in all repositories without justification of cost and benefit (e.g. does existing repository have multiple similar queries? Are team members familiar and comfortable with using Fluent API? etc.)

We define a common interface for all repositories that wish to support Fluent API:

C#
public interface ISupportFluentQuery<TQueryBuilder>
        where TQueryBuilder : IAmQueryBuilder
{
    TQueryBuilder Query();
}

This interface only has one method which returns a class of type TQueryBuilder. A query builder is a class that inherits from a common base class:

C#
public abstract class QueryBuilderBase<T> : IAmQueryBuilder where T : class
{
    protected IQueryable<T> Query;
    protected QueryBuilderBase(DbContext context)
    {
        Query = context.Set<T>();
    }

    public List<T> ToList()
    {
        return Query.ToList();
    }

    public T FirstOrDefault()
    {
        return Query.FirstOrDefault();
    }

    public static implicit operator List<T>(QueryBuilderBase<T> queryBuilder)
    {
        return queryBuilder.ToList();
    }

    public static implicit operator T(QueryBuilderBase<T> queryBuilder)
    {
        return queryBuilder.FirstOrDefault();
    }
}

This QueryBuilderBase in turn implements an interface:

C#
public interface IAmQueryBuilder
{
}

This interface is just a marker interface. It is there so that we can constraint generic type that ISupportFluentQuery can accept. Another alternative to implementing this marker interface is to have ISupportFluentQuery definition to accept a 2nd generic parameter of some common base class/interface that all queryable domain models inherit from.

The generic base class QueryBuilderBase<T> is declared abstract and has protected constructor, so it can only be used by its children. In constructor, it takes a DbContext instance, creates and stores IQueryable for the model class that it can query. This Query will be available to its children for further filtering. This base class also includes 2 implicit operators to convert from the query to result list or result instance without the need for client to call ToList() or FirstOrDefault() explicitly. You will see an example of this usage below.

Let’s take a look at CarQueryBuilder:

C#
public class CarQueryBuilder : QueryBuilderBase<Car>
{
    public CarQueryBuilder(DbContext context)
        : base(context)
    {
    }

    public CarQueryBuilder IsBMW()
    {
        Query = Query.Where(car => car.Brand == CarBrand.BMW);

        return this;
    }

    public CarQueryBuilder IsAvailable()
    {
        Query = Query.Where(car => car.Status == CarStatus.Available);

        return this;
    }

    public CarQueryBuilder WithMinimumPriceOf(decimal minPrice)
    {
        Query = Query.Where(car => car.RentalPricePerDay >= minPrice);

        return this;
    }

    public CarQueryBuilder WithMaximumPriceOf(decimal maxPrice)
    {
        Query = Query.Where(car => car.RentalPricePerDay <= maxPrice);

        return this;
    }
}

It inherits from common base class and declares its own method that is specific to querying car. Each query method will modify the IQueryable instance from the base class and return CarQueryBuilder itself after done. The action of returning CarQueryBuilder itself is the key in creating Fluent API, making the method calls able to be chained to each other. Note that the method names now are very descriptive, and you also no longer need to work with lambda expression.

This CarQueryBuilder only uses Where() filtering, but there’s nothing stopping you from using other EF LINQ methods like Include(), OrderBy(), etc.

Now the final piece is to let CarRepository implement interface ISupportFluentQuery:

C#
public class CarRepository : ISupportFluentQuery<CarQueryBuilder>, IDisposable
{
    private readonly DbContext _context;

    public CarRepository()
    {
        _context = new DemoDbContext();
    }

    public CarQueryBuilder Query()
    {
        return new CarQueryBuilder(_context);
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

Please note that this article focus is not on implementing repository pattern, so we simplify this repository as much as we can for demo purposes, .e.g. the DbContext is created and maintained directly inside repository.

In its Query() method, it creates a new instance of CarQueryBuilder, passing in the current DbContext. So each time user calls Query(), they will have a different instance of query builder.

And finally, usage of Fluent API in client code:

C#
using (var repository = new CarRepository())
{
    List<Car> availableCarsWithMinPrice70
                = repository.Query()
                            .IsAvailable()
                            .WithMinimumPriceOf(70);

    PrintResult("Available cars with minimum price of 70", availableCarsWithMinPrice70);

    List<Car> availableBMWWithPriceBetween60And80
                = repository.Query()
                            .IsBMW()
                            .IsAvailable()
                            .WithMinimumPriceOf(60)
                            .WithMaximumPriceOf(80);

    PrintResult("Available BMW cars with minimum price of 60 and maximum price of 80", 
		availableBMWWithPriceBetween60And80);
}

Here, we just create an instance of CarRepository directly for simplicity. Of course, it’s also possible to register the repository with your favourite IoC container and get an instance of ISupportFluentQuery<CarQueryBuilder>. Or if you don’t want clients to be aware of ISupportFluentQuery, you can create an interface ICarRepository inheriting from ISupportFluentQuery<CarQueryBuilder> and all clients need to do is to get an instance of ICarRepository.

As you can see, the query now is very clear and meaningful. New developers should not have any problem understanding these queries after a quick glance. Common filters like IsAvailable(), WithMinimumPriceOf() are reused in different queries.

Also, note that we need to assign the result to specific type List<Car> in order for implicit conversion operator in QueryBuilderBase to work. If List<Car> is changed to var, you will need to call ToList() or FirstOrDefault() at the end of the method chain explicitly to get the result since the compiler cannot guess what is the type of the result.

The repository supports Fluent API through its Query() method, so the repository is free to implement other interfaces, have all features of a standard repository. Clients still can use standard methods from the repository if they don’t like to use Fluent API.  Because of this, we often think of this Fluent API simply as an extension to repository implementation.

And here’s the result printed in the console:

Image 1

Points of Interest

The initial version of the code has repository class implementing a common repository base class that exposes methods similar to how QueryBuilderBase exposes in this article.

However after careful consideration, we find that this inheritance relationship has many limitations, so we decided to go with using composition instead (not entirely composition in strict definition though, since repository class doesn’t hold a reference to query builder instances). This soon proves to be a correct decision in our opinion: After refactoring, the Fluent API solves previous limitation and becomes a natural extension to the existing repository implementation. We believe this is another example of when composition is preferred over inheritance.

Contribution

Special thanks to Chu Trung Kien for his initial idea and code on using Fluent API in repository.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)