toffik325
toffik325

Reputation: 331

Repository implemented in EF Core: use whole DbContext or only DbSet?

Given that EF Core already implements repository pattern (DbSet) and unit of work (DbContext), can I use DbSet directly in my repository like this?

public class MyEfRepository : IMyRepository
{
    private readonly DbSet<MyModel> _myModels;

    public MyEfRepository(MyDbContext ctx)
    {
        _myModels = ctx.MyModels;
    }

    public MyModel FindById(Guid id)
    {
        return _myModels.Where(x => x.Id == id).SingleOrDefault();
    }
}

And implement unit of work in a following way?

public class MyEfUnitOfWork : IMyUnitOfWork
{
    private readonly MyDbContext _ctx;
    public IMyRepository MyModels { get; }

    public MyEfUnitOfWork(MyDbContext ctx, MyEfRepository repo)
    {
        _ctx = ctx;
        MyModels = repo;
    }

    void Commit() => _ctx.SaveChanges();
}

I'm wondering because every guide I was reading recommended to inject whole DbContext into repository and in methods like FindById access DbSet through it. Since unit of work will commit all te changes, doesn't my approach make more sense? Or I'm not aware of something?

Upvotes: 2

Views: 4602

Answers (1)

Steve Py
Steve Py

Reputation: 35083

The typical approach is to inject the DbContext rather than DbSets as it is a simpler matter to configure your DI container to provide the DbContext than each DbSet from a scoped DbContext instance.

Regarding the comment "The extra layer is for the easier maintenance of the code in case I would like to change from EF Core to some other implementations." I strongly recommend not adopting a repository for this reason. The justification for this stance is that by attempting to abstract away EF, you severely limit the capabilities and performance that EF can provide for your application, or you introduce a considerable amount of complexity and overheads to try and maintain some of those capabilities.

Take a classic example:

public IEnumerable<Customer> GetAllCustomers()
{
    return _context.Customers.ToList();
}

A method like this would load all customers from the DB into memory. What happens if you want to filter the records, or sort, or paginate results? what happens if you just want a count? What about situations where you just need the IDs and a couple of columns?

Even a simpler example:

public Customer GetCustomerById(int customerId)
{
    return _context.Customers.Single(x => x.CustomerId == customerId);
}

This seems safe enough, but what about related data? If a customer has addresses, orders, etc. that I will want to retrieve, we are either relying on lazy loading or kicking off additional queries, how can it know whether it would save time by eager loading related data, and what related data? (vs. eager loading everything)

Pretty soon the code starts including parameters and expressions and such to try and facilitate eager loading, pagination, sorting, etc.. Methods get added to do things like get a count, or project the results to specific DTOs/ViewModels rather than returning entities, otherwise using methods like the above will result in significant performance issues. It becomes a self-fulfilling prophecy. "I abstract away EF in case I need to replace it in the future... I need to replace EF because it's too slow."

The best reasons for using a repository pattern that I can offer boil down to two things:

  1. Making it easier to unit test. (Repositories are generally easier to mock than DbContext/DbSets)

  2. Centralize core filtering rules. For example in soft-delete systems, centralizing IsActive flag filtering. This can also centralize things like authorization checks.

A third reason I personally use is that the repository serves as a good Factory class for validating inputs and returning a "complete enough" entity. For instance there are often required fields and relationships needed before an entity can be saved, along with optional fields. The factory method in the repository ensures all required details are provided and has access to the DbContext to validate/load related references.

The repository pattern I use employs IQueryable to provide the basic abstraction for unit testing while keeping the repositories themselves very simple, lightweight, and flexible.

public IQueryable<Customer> GetAllCustomers()
{
    return _context.Customers.AsQueryable();
}
public IQueryable<Customer> GetCustomerById(int customerId)
{
    return _context.Customers.Where(x => x.CustomerId == customerId);
}

Even for the implied singular select (ById) I return IQueryable as this still accommodates projection via Select or ProjectTo, as well as eager loading scenarios my consumer will know it needs, or simply doing a .Any() if all I want is to check if an item exists.

For instance, my consuming code in one place can use:

var customer = Repository.GetCustomerById(customerId)
    .Include(x => x.Orders)
    // ....
    .Single();

... where that code might look to update some data about the customer and it's relative orders.

while another consuming statement might use:

var customer = Repository.GetCustomerById(customerId)
    .ProjectTo<CustomerSummaryViewModel>(config)
    .Single();

... where this code is only interested in projecting a summary view model of the customer and related data.

If we want to tie in IsActive checks, or ensure that data returned looks at the currently logged in user and any data restrictions etc. the repository is a good place to tie those very universal filters in. If you're not planning on incorporating unit tests, or these types of uniform checks, then a repository doesn't really give you any benefit.

Note that this abstraction does not hide the fact that we are relying on Entity Framework, nor should it attempt to. Trying to hide EF would mean either that we give up a large portion of the power and flexibility that EF can provide us to work with the domain, or we need to explore highly complex code to try and accommodate things that it can provide out of the box. Even clever solutions like passing Expressions as parameters to try and handle sorting, eager loading, or projection to avoid exposing/polluting code with EF-specific or domain specific rules/knowledge are ultimately flawed because they must still conform to EF-specific rules. For instance, those expressions fed to EF must not include method calls or reference unmapped properties etc. They must be aware of the domain and still conform to what EF can understand. The only real way around that is implementing your own expression parser which is really, really not worth it. :)

Upvotes: 10

Related Questions