Just wanted to share a simple approach for Onion Architecture that has saved me a lot of time engaging productivity for everyone on my team.
My simple template for Onion Architecture with .NET 5
Clean architectures are awesome, I don’t have any doubt about that. Highly scalable, fully testable, easy to evolve, easy to adapt to new requirements, etc etc etc.
But unfortunately, I must say that they are not for everyone. If you start adding Input Ports, Use Cases, Output Ports, Presenters, and also adding a DDD approach together with CQRS and Event Sourcing you are adding a kind of steep learning curve for newcomers or people without deep knowledge about good practices, clean code, and related stuff.
And that’s completely fine when you work in a team you need to work as a team, understanding everyone’s point of view and trying to grow all together.
So, how did I try to solve this? How did I promote the usage of good practices in terms of architecture especially when building microservices or serverless functions without overwhelming with a ton of new concepts?
I created this template: It’s just a simple Onion Architecture with CQRS and Event Sourcing. You can use it as you want, you can create a GitHub repository using the template from there or just doing a fork/clone and creating the template from the dotnet CLI.
Just to understand it better I created this diagram which tries to explain everything that can happen in the code:
This is the structure tree of the repository:
C:. │ .gitignore │ Dotnet.Onion.Template.sln │ README.md │ ├───docs │ ARCHITECTURE.md │ CQRS-ES.md │ DDD.md │ HEXAGONAL.md │ SOLID.md │ ├───images │ dotnet-onion-ddd-cqrs-es.jpg │ ├───src │ ├───Dotnet.Onion.Template.API │ │ │ .dockerignore │ │ │ Dockerfile │ │ │ Dotnet.Onion.Template.API.csproj │ │ │ Program.cs │ │ │ Startup.cs │ │ │ │ │ ├───Bindings │ │ ├───Config │ │ │ appsettings-dev.json │ │ │ appsettings-int.json │ │ │ appsettings-prod.json │ │ │ appsettings-stag.json │ │ │ │ │ ├───Controllers │ │ │ TasksController.cs │ │ │ │ │ ├───Extensions │ │ │ └───Middleware │ │ │ ErrorDetails.cs │ │ │ ExceptionMiddleware.cs │ │ │ │ │ └───Properties │ │ launchSettings.json │ │ │ ├───Dotnet.Onion.Template.Application │ │ │ Dotnet.Onion.Template.Application.csproj │ │ │ │ │ ├───Handlers │ │ │ TaskCommandHandler.cs │ │ │ TaskEventHandler.cs │ │ │ │ │ ├───Mappers │ │ │ TaskViewModelMapper.cs │ │ │ │ │ ├───Services │ │ │ ITaskService.cs │ │ │ TaskService.cs │ │ │ │ │ └───ViewModels │ │ TaskViewModel.cs │ │ │ ├───Dotnet.Onion.Template.Domain │ │ │ Dotnet.Onion.Template.Domain.csproj │ │ │ IAggregateRoot.cs │ │ │ IRepository.cs │ │ │ │ │ └───Tasks │ │ │ ITaskFactory.cs │ │ │ ITaskRepository.cs │ │ │ Task.cs │ │ │ │ │ ├───Commands │ │ │ CreateNewTaskCommand.cs │ │ │ DeleteTaskCommand.cs │ │ │ TaskCommand.cs │ │ │ │ │ ├───Events │ │ │ TaskCreatedEvent.cs │ │ │ TaskDeletedEvent.cs │ │ │ TaskEvent.cs │ │ │ │ │ └───ValueObjects │ │ Description.cs │ │ Summary.cs │ │ TaskId.cs │ │ │ └───Dotnet.Onion.Template.Infrastructure │ │ Dotnet.Onion.Template.Infrastructure.csproj │ │ │ ├───Factories │ │ EntityFactory.cs │ │ TaskFactory.cs │ │ │ └───Repositories │ TaskRepository.cs │ └───tests └───Dotnet.Onion.Template.Tests │ Dotnet.Onion.Template.Tests.csproj │ └───UnitTests ├───Application │ └───Services │ TaskServiceTests.cs │ └───Helpers HttpContextHelper.cs TaskHelper.cs TaskViewModelHelper.cs
As you can see, it has the shape of an onion and it has three common layers:
Application layer: where all the business logic is implemented
Domain layer: where our domain is modeled (Task aggregate domain model for example) and some patterns about DDD are there like Repository Pattern (only interfaces, not implementation)
Infrastructure layer: where we implement the code that needs to go outside our application, like for example data access implementation
There can be external layers which are called presentation layers or testing layers. For example, in a common API, the external layer is RestAPI which has the dependencies to handle APIs with ASP.NET Core but in a Cron Job we don’t have APIs, so this external layer is a Console application layer that handles all that we need to start the application and the definition of the workflow to process some data.
I didn’t add any library like AutoMapper or similar because I wanted to remain agnostic about that. If you want to use AutoMapper or any similar NuGet package feel free. The same thing with FluentMediator, I find it really useful and I wanted to handle my Mediator Bus with that package but if you would like to use MediatR you can do it without any problem, you just need to modify the code related to IMediator implementations and definitions.
Going back to the diagram, if you did read my previous articles you will be familiar with The Dependency Rule. It is the most important thing when building a Clean Architecture and summarizing this rule says that the concentric circles represent different areas of software. In general, the further in you go, the higher level the software becomes. The outer circles are mechanisms. The inner circles are policies.
What does that mean?
Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in an inner circle. That includes functions, classes. variables, or any other named software entity.
CQRS and Event Sourcing
This is another thing I wanted to promote with my team (and also more teams). When designing a distributed architecture I think that CQRS (Command Query Responsibility Segregation) and ES (Event Sourcing) can help a lot. Of course, they are not a Silver Bullet, they are just patterns that you can follow if your microservice needs it or not.
And that’s why I included it inside my template because it can be useful for anyone. Also, I didn’t want to introduce CQRS and Event Sourcing with difficult examples with different databases and propagating changes from the Write DB to the Read DB. It is just a simple “DataBase” (there is no database access, just creating mocked data inside the Repository implementation).
If you are a little bit familiar with CQRS you know that there are two flows (models):
Reading (Queries): It is going to use Dependency Injection to the Repository class that it needs to go to the Database or any kind of data source system. The definition of that repository class is an interface, a contract, inside our Domain Layer (you can see that in the upper right corner of the diagram which is an image from the original post about Onion Architecture in 2008). The implementation as I mentioned before, should be outside the Domain Layer, in fact, it is inside the Infrastructure Layer. Why? Because if you are building the Repository implementation in the domain you are coupling the domain to the data source access and also you are breaking The Dependency Rule.
Writing (Commands): If you are modifying the Aggregate state (for example Tasks Aggregate) you need to specify that creating a Domain Command in the code to use CQRS properly. So from this point, if you want to use Domain Commands and CQRS in a way to get the best performance you just need to use a message bus. In our case, we chose to use the Mediator pattern which creates an internal bus and it is the one managing or handling the Domain Commands to use Handlers. These handlers are going to be called by a different thread any time, and if the thread pool is locked, the Domain Command is going to be in the Mediator bus waiting for a thread to be released and execute the rest of the code. This is everything C# magic and we just need to use the Mediator library to use this pattern. Once the code arrives at the specific Handler for any Command with a different thread, it is going to perform the changes in the data source (can be a database, API, etc) using the Repository pattern. But as I mentioned before using Dependency Injection to get a fully testable code following the dependency rule and SOLID principles applied in the architecture.
And in that way, if you need it, once the Handler calls the Repository to perform the changes, you may raise an event that can go to any event store (a queue) and propagate the data to other microservices consuming information from that event store. For example, a Notification microservice that sends an email or something like that.
And that’s it! I tried to keep it simple as much as I could but providing a nice starting point for everyone as I mentioned at the beginning. If you and your team have the skills to implement more complete architectures definitely you should go that way. If not, you can try with simpler solutions and start understanding all concepts, seeing the benefits and the cons of everything, and grow as a software engineer. I hope you enjoyed it and it was clear what I wanted to do. Thanks for reading!
The Tech Platform