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