Every software development project can benefit from code thatโs easy to read, change, and maintain. With code like this, you can easily add new functionality or modify existing functionality without compromising the performance and security of the entire solution.
In this article, we discuss ways of improving an applicationโs code by combining principles of the Clean Architecture and Command and Query Responsibility Segregation (CQRS) approaches.
We use a .Net Core project as an example, so this article will be particularly helpful for web application development teams that want to improve the quality and cohesion of their code in .Net Core solutions.
Contents:
Exploring Clean Architecture and CQRS
Applications with highly interdependent code sections are challenging to enhance and support. One reason for this is that the code gets cluttered over time and the slightest change might lead to devastating consequences, including application failures and security breaches. To improve the quality and maintainability of your applicationโs code, you can divide it into separate, independent layers that work with the user interface (UI), business logic, and data.
You can follow Clean Architecture and Command and Query Responsibility Segregation (CQRS) principles to make sure that code areas responsible for the user interface, business logic, and data donโt depend on each other and allow for a quick introduction of changes.
The Clean Architecture approach was introduced by the rock-star of software development: Robert C. Martin, also known as Uncle Bob. This approach requires dividing application code into several independent layers:
- Domain entities
- Use cases
- Controllers
- Databases
- User interface
- External services
Such separation of concerns can help you build a flexible solution thatโs easy to test and has no dependencies on the framework, databases, and external services.
However, to ensure maximum code clarity, especially in long-lasting and complex projects with lots of cluttered code, we suggest combining Clean Architecture principles with the CQRS approach, which aims to separate operations of reading data from operations of writing it.
While adding an extra layer to your application, the CQRS approach brings several significant benefits:
- Single responsibility โ Commands and queries in your applicationโs code can only have one task: to retrieve or change data.
- Decoupling โ A command or query is completely decoupled from its handler.
- Scalability โ The CQRS pattern is flexible in terms of how you organize your data storage. You can use separate read/write databases for commands and queries with messaging or replication between databases for synchronization.
- Testability โ Design simplicity makes your command or query handlers easy to test. You donโt need to write tests for handlers that donโt have cross-service, cross-module, or any other cross-calls.
CQRS and Clean Architecture fit each other perfectly since domain workflows know nothing about the existence of commands and queries. At the same time, we can still call as many business logic methods as we need in a single command or query.
Letโs see how you can combine these two approaches to improve the cohesion and flexibility of application code. As an example, weโll implement Clean Architecture in a .NET Core web application.
Ready to enhance your software?
Reach out to us now for expert assistance in .NET development and drive the maintainability and performance of your solution.
Clean architecture for a .NET Core application
Say we have a web application processing online orders. Our goal is to isolate this applicationโs domain, infrastructure, and database so we can change them independently from each other. To do that, we create separate projects for:
- Business logic, which contains a set of specific repositories and services
- Infrastructure, which has a common logger and validator implementation and a set of commands, queries, and validators
- Database interactions that rely on entity classes, DBContext, and generic repositories
- A set of controllers in the form of an API
In the following sections, we discuss different ways of applying Clean Architecture and CQRS principles based on the example of this application.
Designing commands and queries with MediatR
According to its creators, MediatR is a library meant for decoupling the operations of in-process message sending from message handling.
To work with MediatR, youโll need a dependency injection (DI) container. There are different DI containers for you to choose from and examples of their use on MediatRโs GitHub page. But at the beginning of our project, we decided not to use any of those containers. Instead, we chose the built-in .NET Core DI container, as its capabilities met the project requirements. All we needed to do was install the MediatR package and call the AddMediatR method with the following command:
public void ConfigureServices(IServiceCollection services)
{
services.AddMediatR(typeof(Startup));
}
There are two types of messages in the MediatR library:
- request/response โ Such a message is processed by one handler, which usually returns a response.
- notification โ This message can be dispatched by multiple handlers that return no response.
In our project, we only used request/response messages, so we had a bunch of classes implementing the IRequest<T> interface:
public class OrderQuery : IRequest<IEnumerable<order>>
{
#region Parameters
public int OrderNo { get; set; }
public string Phone {get;set;}
// and so on
#endregion
IOrderService _orderService;
public OrderQuery(IOrderService orderService)
{
_orderService = orderService;
}
public class OrderQueryHandler : IRequestHandler<OrderQuery, IEnumerable<order>>
{
public async Task<IEnumerable<order>> Handle(OrderQuery request, CancellationToken cancellationToken)
{
if (request.OrderNo > 0)
{
return await _orderService.FindByNo(request.OrderNo, cancellationToken);
}
return _orderService.FindOrders(request.Phone, cancellationToken);
}
}
}
Next, we place class OrderQueryHandler : IRequestHandler<OrderQuery, string>
inside the Request class. While this isnโt obligatory, we used this approach to encapsulate parameters and the handler in the same class.
Since messages in our project are sent by controllers, there was a mediator responsible for calling the necessary handler injected into each controller:
public class OrderController : Controller
{
private readonly IMediator _mediator;
public OrderController (IMediator mediator)
{
_mediator = mediator;
}
}
After an HTTP request is mapped to the controllerโs method, the method sends an encapsulated command or query:
[HttpGet("find-orders")]
public Task<IEnumerable<order>> FindOrders(int orderNo, string phone) =>
_mediator.Send(new OrderQuery()
{
OrderNo = orderNo, Phone = phone
});
Now that weโve reviewed the process of decoupling messages, letโs see how to implement implicit logging in a .NET Core Clean Architecture.
Read also
Radzen Rapid Web App Development Platform Review
ะกreate efficient software without a single line of code. Leverage our step-by-step guide to Radzen to quickly deliver top-notch apps.
Implementing implicit logging with MediatR
Logging and profiling are absolutely necessary processes for any modern application. They provide detailed information on where a problem occurred and the data that caused it, helping developers and QA professionals cope with issues like user errors and reduced performance.
Thereโs a wide range of logging levels for both applications and user actions:
- For applications โ from “warning” to “exception only”
- For user actions โ from “log any action a user takes” to “log only destructive actions”
Developers may choose to store these logs in any place and any format: plain text files, databases, cloud and external services. To do so, they can use different libraries including NLog, Serilog, and log4net.
The main rules of implementing logging and profiling are:
- Make sure that logging and profiling donโt affect your domain.
- Be ready to change your logging and profiling approaches anytime.
As we wanted to follow the recommendation of deferring any framework selection when building a Clean Architecture in .Net Core, we used a built-in .NET Core logger. The best way to use this logger in code was to inject the ILogger interface into each class and call the LogInformation method when needed.
However, we decided to make logging of application events and user actions implicit so we donโt need to call the logging methods from each class separately. And to do that, we built a custom command and query pipeline.
Starting with MediatR 3.0, developers can add custom actions (behaviors) that will be executed for any request. To enable implicit logging, we needed to implement the IPipelineBehavior<TRequest, TResponse> interface and register it in the Startup class. You can find detailed instructions for adding a custom pipeline to your project in this MediatR guideline.
This technique can be used for other cases as well. In our project, we also applied it to separate validation operations from business logic. Letโs see how we can improve our work with validation in the next section.
Using FluentValidation for parameter validation
Once weโve written our queries and commands, we need to validate the parameters that come along. The easiest way is to write straightforward checks like if (parameter != null) {...}
, but doing so makes the code harder to read.
A slightly better option would be to use tools like Guard or Throw. In this case, the method calls would look somewhat like Guard.NotNull(parameter)
or parameter.ThrowIfNull()
.
Following the principles of Clean Architecture in .Net Core apps, anything besides business logic should be plugged in, so we used the FluentValidation library. This library allows for implementing custom parameter validation, including complex asynchronous checks.
When working with FluentValidation, we must put all validation in a separate file for each command or query. Letโs see how this works in practice.
Here we use the OrderQuery class from the previous section as an example. Say we want to find an order using either a clientโs phone number or a unique order number. To validate these parameters, we create a separate OrderQueryValidator class:
public class OrderQueryValidator : AbstractValidator<orderquery>
{
public OrderQueryValidator ()
{
RuleFor(x => x.Phone).NotEmpty().When(x => x.OrderNo <= 0);
RuleFor(x => x.OrderNo).GreaterThan(0).NotNull().When(x => x.Phone == null);
}
}
In this way, validation is separated from the business logic, which improves the readability of our web applicationโs code.
To summarize, when adding a new API endpoint (if everything is set up in the Startup class) to a web project, we need to make sure to:
- Have repository methods to retrieve or modify data
- Have necessary functionality in the applicationโs business logic
- Create a command or query
- Create a typed handler
- Create a typed validator
- Add a controller (if it doesnโt exist)
- Add a method to the controller that will send the command or query to a mediator
- Implement logging behavior (if it doesnโt exist)
It may seem annoying to create lots of files when separating concerns within your applicationโs architecture. If you follow the principles described in this article, youโll have two more files for each controller method. But in this way, your code will be well-structured, easy to read, and compliant with the single responsibility principle.
The problem of creating files can be easily solved by writing a custom script that creates templates for the Command, CommandHandler, and CommandValidator classes.
Related project
Developing a Custom MDM Solution with Enhanced Data Security
Explore how Aprioritโs team created a custom MDM solution for centralized and secure tablet management.
Conclusion
In this article, we showed how you can build a Clean Architecture for a .Net Core application and implement key CQRS principles to make your applicationโs code easy to read, test, and improve. The steps described above allowed us to separate business logic from other application layers โ an obligatory requirement of the Clean Architecture approach.
Now, our example application has logging and validation plugged in, and the domain is easy to test, as code transparency allows us to skip some trivial checks. Furthermore, we can create any command or query we need without having to change the existing domain.
At Apriorit, we have dedicated teams of experienced web developers. With wide expertise, including .NET development, we are ready to help you turn any idea into a reliable IT product.
Looking for an expert web development team?
Let Apriorit’s professionals build a transparent, cohesive, and flexible application that will meet your business needs and engage your audience.