1

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?

3
  • 2
    But you are injecting the context into the repository. For the DI pattern it doesn't matter how it's used inside. What you've got is a very common setup. The only question that remains is: do you really need this extra layer? Most of the times the answer is: no. Commented Jan 9, 2022 at 21:21
  • @GertArnold thank you! Indeed you are right, I should have said "assigned" DbSet directly rather than "injected". 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. The thing that worried me was why would people work on the context directly in their repositories if they in fact dont need commiting changes here? Commented Jan 9, 2022 at 22:39
  • 1
    If it's only for a transition that might occur one day in the future, IMO this is way too much dead weight in your code you have to drag along all the time. Your code will be affected by EFs idiosyncrasies anyway. A transition to another data provider will never be a smooth process. Commented Jan 10, 2022 at 7:54

2 Answers 2

10

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

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you Steve for your comprehensive reply! That's actually a good point and I ran into similar issues with some other project. I'm still learning, so in fact it's even hard for me to judge accurately which technology would cover my needs the most so I wanted to have such back doors for easier change. I wanted to implement DDD with CQRS and hexagonal architecture - so isn't your advice kind of contradictory to Ports & Adapters pattern? For my blogging platform I would need to only Add, Delete, Update and Search (using some DTO + dynamic query builder).
For those patterns I wouldn't normally use the Repository itself as the boundary for the DTOs. EF would fall under Persistence so an adapter would wrap messages to/from EF entities and the core domain logic. That adapter would reference the repository and leverage projection to build relevant DTOs, and map DTOs back to their respective entities. That way the entire adaptor can be substituted. (I.e. file store, or non-EF data store)
If you expose IQueryable, you allow far too much flexibility that is specific to EF. Your services shouldn't be defining complex DB queries (whether writting in SQL or LINQ)
There are always trade-offs. The alternative is a straight-jacket that leads to inefficient querying, or moving business logic into Repositories/Domain boundaries where it's harder to test, or using "fake" abstractions like passing Expressions. (which have to conform to EF-isms anyways) If Entity Framework is chosen to serve in an application it should not be hidden under layers of abstraction. Doing so just introduces performance problems that weirdly justify the choice to abstract so it can be "swapped out" when it seems too slow/heavy. A self-fulfilling prophecy in architecture design.
-2

I like for Repository to receive DbContext, and to very generically do the repository ations on their types, i.e. Get<T>, Single<T>, AddOrUpdate<T>, Delete<T> or whatever, some or all of which predicated. The trick is to then conjure up an appropriate DbSet<T> at the moment it is required, I keep mine in an IDictionary<Type, IListSource> collection for example, and only generate them as I need them. In EF parlance, I don't know how kosher that is, per se; the trick is, I think, to negotiate an apporpriate navigation bridging the gap from domain instances to EF entities, that are subsequently marked for appropriate Add, Update, or Delete. Which is easy enough to facilitate from domain with things like:

interface IModel { bool IsDeleted { get; set; } } interface IModel<TId> : IModel { TId Id { get; set; } // As a function of Id, usually. bool IsPersistent { get; } } 

Along these lines, at any rate. Then a given AddOrUpdate<T> may receive any such IModel and conduct the appropriate set of navigations.

Of course the trick is to inject the domain specific navigation, perhaps as a callback to the IRepository, such that it can do so in a truly and properly inversion of controlled manner, and keep repository very, very generic and reusable.

Then there is the matter of operating whether in a disconnected environment. Mine generally is, I get the set of data, and discard the repository, keep my database clean from connections. So then I must re-attach instances during that negotiation process, navigating to do the correct updates.

Other thoughts, nuggets, pearls?

1 Comment

Please keep in mind that Stack Overflow is not a discussion forum. It's all about questions and (only) answers. What you write here is very tentative, even vague, and discussion-driven. IMO the only answer to the direct question "can I use DbSet directly in my repository like this?" is: yes. The rest is opinion-based.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.