Logo
blank Skip to main content

Combining Clean Architecture and CQRS Principles in a .NET Core Application: A Practical Example

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.

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.

Traits of a clean architecture solution

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:

Benefits of Command and Query Responsibility Segregation (CQRS)
  • 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
architecture for a .NET Core application

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:

C#
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: 

C#
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:

C#
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:

C#
[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.

Learn more

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:

  1. Make sure that logging and profiling donโ€™t affect your domain.
  2. 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:

C#
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. 

Project details
Developing a Custom MDM Solution with Enhanced Data Security

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.

Have a question?

Ask our expert!

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.