Search code examples
c#entity-framework-corerepository-patternencapsulation

Encapsulating the query logic in EF Core


Recently, I came across a blog post regarding the encapsulation of queries in EF Core. This approach is somewhat different from the repository pattern which is much more familiar to me.

In repository pattern, I follow an approach like this,

public interface ISomeRepo
{
    IEnumerable<string> GetAll();
    // Some other queries
}

public class SomeRepo : ISomeRepo
{
    public IEnumerable<string> GetAll()
    {
        // Get data from database
    }
    // Implementation of other queries
}

But the approach which is described in the blog post is something like this,

public interface IModelQuery<out TResult, in TModel>
{
    IEnumerable<TResult> Execute(TModel model);
}

public class PartialMatchQuery : IModelQuery<string, IEnumerable<string>>
{
    private readonly string partial;
 
    public PartialMatchQuery(string partialString)
    {
        partial = partialString;
    }
 
    public IEnumerable<string> Execute(IEnumerable<string> model)
    {
        return model.Where(s => s.ToLower().Contains(partial));
    }
}

And querying the data source this way,

static int Main(string[] args)
{
    var strings = new List<string>() // I used an IEnumerable<T> implemented collection for simplicity
            {
                "Abhc", "Bmhh", "Csjudsm", "Dsjuh", "Ejhduhb", "Fmjasgh"
            };

    var query = new PartialMatchQuery("m");
    var result = query.Execute(strings).ToList();

    return 0;
}

I feel like this "new" approach is flexible compared to the repository pattern which contains a set of "strict" operations like GetAll(), GetById(), Modify() etc. On the other hand, with this IModelQuery thing, I could end up with 100 classes if I have 100 different queries. I searched the web for some real-world implementations of this new approach (at least new to me) but couldn't find any.

So, here are my questions:

  1. Is it really worth to use this new approach over the repository pattern bearing the cost of " one class per one query"?
  2. Are there any side-effects (that I had missed) of using this new approach that affects the maintainability or scalability?

Solution

  • I read the two-part blog and only the second part gets into implementing this with EF. Honestly all I see is a veritable crap-ton of wrapper code for what can simply be done via working with IQueryable.

    In the micro-service world, patterns like this and CQRS may be useful where operations are scoped down to single entities. For most line of business applications drilling down to something like this would be prohibitively expensive and limiting.

    It will need even more code to leverage asynchronous operations, and his test code implementation will not work with async operations. This also does not work with projection, nor accommodate things like eager loading dependencies when working with more complex entity structures. In short, it is an exercise in futility. Most of the examples you will find on blogs and such cover only ridiculously simple use cases but you will soon discover in a real world solution that there are huge gaps that require even more code to try and address, more complexity, deviating from the "pattern", or ending up comping to a conclusion that "EF is too slow" because shoe-horning it into some abstraction that only succeeds in effectively crippling it.

    EF does not need a repository pattern, it has one in the DbSet class. It does not need a Unit of Work pattern, as it has one in the DbContext class. This isn't to say that you cannot extract a Unit of Work or Repository pattern over it, but you need to be careful about the reason for the abstraction and choosing an implementation that does not cripple what EF can do.

    My advice from reading that article? Avoid it. It needs more work to handle asynchronous operations, it does not easily accommodate projection (Select / ProjectTo etc.) and it does not accommodate eager loading (Include) or things like pagination without more methods, parameters, and/or complexity.

    Absolutely avoid Generic Repository patterns with IEnumerable<TEntity> GetAll() and the like for all of the same reasons.

    For most projects, especially for those developers are writing when starting out with EF, just use the DbContext and DbSets and learn to count on projection for read operations, and eager loading for updates. Avoid things like detached entities including passing entities around between service layers, use DTOs or ViewModels that are projected. If you later develop to a point where you want to leverage unit testing or are working on a system like a multi-tenant SaaS or something with a soft-delete implementation where you don't want to rely on EF's core filtering rules for soft-delete and have core domain rules then I can recommend a repository pattern, but leveraging IQueryable<TEntity> rather than returning IEnumerable<TEntity> or even TEntity directly. This type of repository can be easily mocked for unit testing and can encapsulate core domain rules, while remaining completely flexible to working with async operations, projection, eager loading, pagination, filtering, and sorting.

    If working on a micro-service solution where it makes sense to break things down to individualized commands or queries that work completely independently and can be latched onto with observers and the like, then go hard with something like this. ;)