It is a common practice to inject DbContext into the service layer, it is however not instantaneously a GOOD practice on its own.
I want to keep my controllers as clean as possible, so I want to put all the business logic inside the service layer
That statement can be contradictory, you want everything in one place and you want to keep it clean as possible... This is the major driving argument behind implementing your own repository.
Repository and Unit of Work
- A key goal of the Repository Pattern is to try and encapsulate data transformation and query logic so that it can be reused and separated from the database design itself
- Unit Of Work is concerned with tracking changes and coordinating how and when those changes are committed to the database, perhaps across multiple repositories.
EF DbContext manages a Unit Of Work implementation over a Repository of Business Domain Entities that are mapped to Database Schema Tables. EF therefore represents both UOW and Repo.
When Is EF A good Repo?
Just because EF is A Repo, does not mean it is THE Repo for your solution, EF can be the perfect repo for the business domain logic has direct access to it, disconnected service architectures can however get in the way of this, unless your entire business logic is encapsulated in the service layer, so every button click and decision point in your client/UI is mapped back to the API, then there some of the business logic spills over into the client side, so an EF based service layer requires a lot of plumbing if you were to expose all of the functionality that the client needs in a meaningful way.
If you end up mapping all or the majority of EF model classes into DTOs or have an entirely different business model that you want to expose to clients, then to do all of this in your API Controller classes can easily become a huge management and maintenance issue, with APIs Controllers you really need to separate the routing and De/Serialization of requests and responses from the actual logic, using another Repo to encapsulate the logic implementation from the transport (the controllers) is a good thing, this usually means that you would NOT inject the DbContext, unless it was simply to pass through to the Repo for that controller.
If the EF Model is not being exposed by the controller, then it is better to avoid injecting the DbContext into the controller, as it will encourage you to do too much in the controller itself.
Lets consider when it IS a good practice to inject the DbContext into the service layer:
In a tiered solution with a Service Layer, if the EF Model represents the main DTOs that will be transferred between the Service and the Client, then EF is a good choice to use in the controllers directly. In fact if that is your goal, then you should consider OData as a service framework as it does this and provides an additional layer of abstraction (the OData Model) that is initially mapped to the EF model but you can easily extend elements to implement custom business logic or structures with theadded benefit of exposing a convention based standard interface for querying data from the clients.
OData basically maps HTTP queries to deferred EF queries, which are in turn mapped to SQL queries (or whatever your chosen backend is). If you use the Simple or Connected OData Client frameworks then Client-side linq statements can be passed through (albeit indirectly) to the database for execution.
When your EF Model represents the bulk of the DTOs exposed from the service and consumed by the clients, then it is a standard practise to inject the DbContext into the Controller definitions, OData is an Implementation that does this with minimal effort and provides a client-side implementation to manage UOW on the client as well!
Why do I need another abstraction layer
As mentioned above, due to the disconnected nature of things, the service layer almost always ends up forming its own abstraction layer, whether you choose to identify or implement them or not, the service layer imposes security and structure constraints on the calls to our business logic. Sometimes we transform data on the service side for efficency or reduction in bandwith, other times to deliberately hide or withold confidential or process critical data, or prevent the client from updating certain fields.
There is also the question of protocols, most modern APIs even add Content Negotiation such that the service can be consumed by different formats as specified by the client. Your controller code will get extremely heavy and dare I say dirty whne you start to tweak some of these factors.
You can gain a great deal of flexibility and interoperability from creating your own repo to manage these interactions, to separate transport and bindings from your business logic.
I wouldn't want to create unnecessary abstractions such as a repository layer.
In a small project or with a very small team it may seem unnecessary, but many of the benefits to creating your own repo will be realized later, either when on-boarding new developers, extending your project to more or new types of clients or perhaps importantly, when the underlying frameworks or operating systems change and you need to adapt to them.
Another abstraction layer shouldn't mean that you are introducing another performance bottleneck, in most cases there are ways to abstract the business logic in a way that either improves the throughput or is effectively pass through. Any performance loss, if observed should either be fixed or it should be easily justified in the net gains to the user or SDLC.
With service based architecture it is normal to see these abstractions:
- Database
- Data Model
- ORM
- Business Logic
- API Transport (Server)
- API Transport (Client)
- ViewModel
- View
If you utilise SQL Server, EF and OData, then the only elements you need to explicitly code yourself are:
- Data Model
- Business Logic
- ViewModel
- View
In the end, it is not a question of should the DbContext be injected into the controller, its more of a question about why? Its the Business Logic side of things that matters, this usually requires your DbContext, if you choose to merge your business logic with the API controllers, then you are still managing the business logic, its just harder to identify the boundaries between what is transport related and what is actual business logic.
If the business logic needs to be a separately defined repository or can be an extension of the DbContext is up to you, It will depend greatly as reasoned above on whether you expose the EF Data Model objects through to the client at all, or if all interactions need to be transformed to and from DTO structures. If its ALL DTOs and Zero EF model being exposed, then this falls 100% into the realm of a Repo. Your controllers can be the repo implementation, but everything said before this suggests that is not the best idea for the long term stability of your development team or the solution itself.
As an example, I often write my business logic as extension methods to the EF Model. I use the EF as the repo in this case, this allows many other server side processes to access the business logic without having to go through the HTTP activation pipeline when it makes sense to do so.
T4 templates are used to generate the service layer including OData Controllers from the EF model and the extension methods that are specifically decorated with attributes to identify those moethds that should be available to external clients.
In this implementation, the DbContext is injected to the controllers, because that is the entry point to the business logic!
The client-side projects use the OData Connected Service to generate clien-side proxies providing a strongly typed and linq enabled context that is similar to the DbContext but with the business logic carefully constrained within it.