Reputation: 135
I am trying to implement the onion architecture in a .NET Core 3.1 Web API project with EntityFramework as the ORM.
Since I am just learning the onion architecture, I am having a little problem with how to follow it's rules for certain areas. One of them is the dependency injection.
Lets say we have the following rings (Outer to Inner):
From what I understand in the onion architecture, separate concerns in the same layer should not depend on one another. Therefore, in the Infrastructure ring, and more specifically in the API project, I understand how to hookup the DI for my services, since those services (interfaces and implementations) are in the lower rings. However, I also need to setup DI for my DbContext which is defined in the Persistence project of the same ring. Also additional third party tools which will also live in the same ring, also need to be hooked up through the DI.
There are two solutions I see to this:
To just summarize to my question in a different way: In an onion architecture, if you are using DI, the DI looks like it should be in the Infrastructure ring and it looks like the DI needs to reference everything in the Infrastructure ring. Does DI have to be handled as an exception in onion architecture? How to handle this?
Upvotes: 2
Views: 1966
Reputation: 135
Here is how I solved my situation. I think it is pretty clean although I have to test a lot of more scenarios/use cases before I can safely adopt this solution.
In addition to my API project, I also created a DI project. This DI project is the composition root project. It has dependencies on everything (application, domain, etc) It is the upper part (the outer sub-ring) of the Infrastructure ring. Then, in my API project, I am referencing the DI project and calling an extension method from there to tie up all my contracts to implementations. In addition, I had to add a second web application for something else as part of this same solution and I was able to use the same method, which is good.
Upvotes: 0
Reputation: 29262
Here's my understanding:
When compiling, the domain does not depend on anything out of itself. Therefore, for example, the domain would not depend on the persistence project. It would depend on abstractions defined within the domain itself.
Your persistence (for example, concrete repositories) would fulfill the dependencies defined in the domain. In real-world terms that usually means they would implement interfaces defined in the domain.
The composition root then configures the application so that the concrete implementations are matched with the abstractions defined within the domain. That's the DI configuration.
That's the simplest version. You can add more complexity to that if you need it. The only part that wouldn't change is that the domain is self-contained. Here are some other ways that the complexity could grow if needed:
But the critical detail is that in any case the domain itself is self-contained. We wouldn't inject an interface defined outside the domain into a domain class. The domain defines its own abstractions, and then classes defined outside of the domain fulfill those abstractions.
Your API can depend on the persistence project. Your API is not the domain. Your API startup would depend on the domain and the persistence and configure the DI container to supply classes from the persistence project to fulfill dependencies defined in the domain.
The API is like the opposite of the domain. The domain depends on nothing outside of itself. Other dependencies point inward. The repositories, for example, implement abstractions defined by the domain.
The API on the other hand, ultimately depends on everything. It uses the DI configuration to supply dependencies (like persistence) to the domain, which means it's going to depend on all of them.
Here's (in my opinion) the biggest change in thinking:
We have a tendency to write concrete classes for data access, to call external APIs, and do other things that are peripheral to the domain. Then we put interfaces on them. Those interfaces often look like an afterthought. It's like we're just creating interfaces that mirror those classes. If we add something to the class, we add it to the interface.
Then we take those interfaces and start injecting them into our domain. That's where things get messy. Those interfaces have nothing to do with our domain. They're just mirror images of classes that exist outside the domain. It makes the domain harder to test. Interface Segregation is violated. We find ourselves mocking parts of interfaces that have nothing to do with our domain.
All of that goes away if we design our abstractions from the point of view of the domain classes that depend on them. For example, if our domain class needs to retrieve data from a repository, we define a repository that models exactly what that domain class needs - nothing more, nothing less. We don't take some generic all-purpose repository interface and jam it into our domain.
We might still have some generic all-purpose repository. That's okay. But we adapt it to that domain repository interface. The domain doesn't know about it. All it knows about is the abstraction defined in the domain. The domain class depends on small, segregated abstractions it defines for its own purposes. It owns them. That keeps it simple and makes it really easy to test.
Upvotes: 2