Keeping the repository interface clean
Published on
The repository pattern is being blamed quite often. The most popular reason for that is an uncontrolled growth of the interface.
In the simple scenario, we have an interface like this one:
public interface IClientRepository
{
Client Get(int id);
IEnumerable<Client> GetAll();
}However, models are never that simple, and every client might have orders, addresses, contact details, and other nested properties.
We don’t want to load our database with unnecessary joins and Entity Framework has a nice tool for that.
When we need to retrieve only client’s orders, we can include it into the query:
context.Clients.Where(client => client.Id == id)
.Include(client => client.Orders);When we need just addresses:
context.Clients.Where(client => client.Id == id)
.Include(client => client.Addresses);And the combination:
context.Clients.Where(client => client.Id == id)
.Include(client => client.Orders)
.Include(client => client.Addresses);However, the IClientRepository grows exponentially and quickly becomes an ugly monster:
public interface IClientRepository
{
Client Get(int id);
Client GetWithOrders(int id);
Client GetWithAddresses(int id);
Client GetWithOrdersAndAddresses(int id);
IEnumerable<Client> GetAll();
}It’s not a nice solution, for sure: lots of code duplication and very unclear interface.
A typical way to avoid it is to use CQRS pattern and encapsulate the logic into the Query object.
At my current project, we’re building microservices, and a CQRS looks like overkill. It doesn’t stop us from borrowing some ideas, though. Let’s remove all these GetXXXX methods and add one more parameter to the Get method.
public interface IClientRepository
{
Client Get(int id, FetchPaths<Client> fetchPaths = null);
IEnumerable<Client> GetAll(FetchPaths<Client> fetchPaths = null);
}
The FetchPaths is a simple generic class which holds the collection of expressions that we’re going to use to build our chain of Include statements.
public sealed class FetchPaths<T>
{
public IEnumerable<Expression<Func<T, object>>> Paths
{
get;
private set;
}
public FetchPaths(Expression<Func<T, object>> path)
: this(new[] { path })
{
}
public FetchPaths([NotNull] IEnumerable<Expression<Func<T, object>>> paths)
{
if (paths == null)
{
throw new ArgumentNullException("paths");
}
Paths = paths;
}
public FetchPaths<T> Concat([NotNull] FetchPaths<T> other)
{
if (other == null)
{
throw new ArgumentNullException("other");
}
return new FetchPaths<T>(Paths.Concat(other.Paths));
}
}Now we’re ready to declare the following static class:
public static class ClientFetchPaths
{
static ClientFetchPaths()
{
Orders = new FetchPaths<Client>(client => client.Orders);
Addresses = new FetchPaths<Client>(client => client.Addresses);
OrdersAndAddresses = Addresses.Concat(Orders);
}
public static FetchPaths<Client> Orders { get; private set; }
public static FetchPaths<Client> Addresses { get; private set; }
public static FetchPaths<Client> OrdersAndAddresses { get; private set; }
}So that we can get the clients including all the necessary child objects.
var clients = ClientRepository.Get(id, ClientFetchPaths.OrdersAndAddresses);I want to mention that so far we don’t have any Entity Framework dependency here. That means that we can keep this code (interface, models, and FetchPaths) in the same assembly and reference it from the unit test project.
Now the repository implementation will be clean as well:
context.Clients.Where(client => client.Id == id)
.Fetch(fetchPaths));Where Fetch is an extension method for IQueryable.
public static class FetchExtensions
{
public static IQueryable<T> Fetch<T>(
this IQueryable<T> queryable,
[CanBeNull] FetchPaths<T> paths) where T : class
{
if (queryable == null)
{
throw new ArgumentNullException("queryable");
}
if (paths == null)
{
return queryable;
}
return paths.Paths.Aggregate(queryable,
(current, expression) => current.Include(expression));
}
}