A demo on Clean Architecture with CQRS and Repository Pattern in .NET Web API
Application architecture

A demo on Clean Architecture with CQRS and Repository Pattern in .NET Web API

Clean Architecture

Clean Architecture lives on the dependency inversion principle. In general business logic depends on the data access layer or infrastructure layer. But in clean architecture, it is inverted, which means the data access layer or infrastructure layers depends on the business logic layer(which means the Application Layer).

To learn more about Clean architecture, I suggest you read my article about Getting Started with Clean Architecture in .NET Core .

Implementing Clean Architecture

Create projects

To start, create an empty solution and add the following projects to it:

  • Domain(Class Library)
  • Application(Class Library)
  • Infrastructure(Class Library)
  • WebApi(Web API)

Add references

To implement Clean Architecture, add references to the projects.

  • Domain should be an independent project.
  • Application should depend on Domain.
  • Infrastructure should depend on Application and Domain.
  • WebApi should depend on Infrastructure and Application.

CQRS Pattern

CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates the read and write operations of an application. It uses distinct models for commands (write) and queries (read), allowing for scalability and performance optimization.

By decoupling the models, developers can independently optimize each side for their specific requirements. While it introduces complexity, CQRS offers benefits in terms of scalability, performance, and maintainability.

No alt text provided for this image
CQRS Pattern

MediatR Pattern

MediatR is a popular design pattern that utilizes handlers to handle Commands and Queries.

By leveraging MediatR, developers can significantly reduce boilerplate code, such as injecting multiple services into the controller. MediatR offers a unified entry point that expects a RequestModel, allowing the corresponding handler to be invoked based on the request. Acting as a centralized communication hub, MediatR ensures that all handler invocations from the controller go through it. Therefore, it is widely recommended to use MediatR in conjunction with the CQRS pattern.

MediatR
MediatR Pattern

Installation of Packages & Registration of Services

To configure projects for DI(Dependency Injection) and to implement FluentValidation and MediatR for CQRS, we need to install MediatR and FluentValidation.DependecyInjection package in the Application project and Microsoft.Extensions.DependencyInjection in the Infrastructure project. We will also need to install MediatR.Extensions.DependecyInjection in the WebApi project

MediatR - Application project:

Install-Package MediatR        

FluentValidation.DependecyInjection - Application project:

Install-Package FluentValidation.DependencyInjectionExtension        

Microsoft.Extensions.DependencyInjection - Infrastructure project:

Install-Package Microsoft.Extensions.DependencyInjection        

MediatR.Extensions.DependecyInjection - WebApi project:

Install-Package MediatR.Extensions.DependecyInjection        

In the Application project create DependecyInjection, a public static class, and add the following code:

using FluentValidation
using Microsoft.Extensions.DependencyInjection;

namespace Application;

public static class DependencyInjection
{
? ? public static IServiceCollection AddApplication(this IServiceCollection services)
? ? {
? ? ? ? var assembly = typeof(DependencyInjection).Assembly;

? ? ? ? services.AddMediatR(configuration =>
? ? ? ? ? ? configuration.RegisterServicesFromAssembly(assembly));

? ? ? ? services.AddValidatorsFromAssembly(assembly);

? ? ? ? return services;
? ? }
};        

Add this code to the Infrastructure project DependecyInjection class:

using Microsoft.Extensions.DependencyInjection

namespace Infrastructure;

public static class DependencyInjection
{
? ? public static IServiceCollection AddInfrastructure(this IServiceCollection services)
? ? {
? ? ? ? return services;
? ? }
};        

And add the following code to the Program.cs file in the WebApi project:

builder.Service
? ? .AddApplication()
? ? .AddInfrastructure();        

In this step, we configured our layers with Dependency Injection as well as finished the setup for MediatR and FluentValidation.

Repository Pattern

The Repository Pattern is a software design pattern that separates the data access logic from the business logic in an application.

It provides a layer of abstraction by defining a consistent interface for data operations. This decoupling allows for easy maintenance and testability of the codebase. The pattern promotes code reusability by encapsulating data access details within repositories, making it easier to swap or extend data storage implementations.

By adopting the Repository Pattern, developers can achieve a clear separation of concerns and improve the overall modularity and flexibility of their applications.

No alt text provided for this image
Repository Pattern

Creating Repository

Data Access

To get started with repositories, we need to sort out our Data Access Layer. First, we need to create a Model for the data.

In the Domain project, create an Entities folder and in that folder create a Person(example) class which should be public and sealed.

namespace Domain.Entities;

public sealed class Person
{
? ? public int Id { get; set; }

? ? public string Name { get; set; } = string.Empty;

? ? public string Email { get; set; } = string.Empty;
};        

The second step is to create a DbContext. We do this in our Infrastructure project. To create DbContext, first, we need to install a couple of packages:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer

To install these packages, use the following commands:

Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer        

Create a PersonDbContext class in the Infrastructure project:

using Domain.Entities
using Microsoft.EntityFrameworkCore;

namespace Infrastructure;

public class PersonDbContext : DbContext
{
? ? public PersonDbContext(DbContextOptions options) : base(options)
? ? {
? ? }

? ? public DbSet<Person> Person { get; set; }
};        

Repository

To start with repositories, add an Abstractions folder to the Application project. In there create an interface for the person repository - IPersonRepository.

using Domain.Entities

namespace Application.Abstractions;

public interface IPersonRepository
{
? ? Task<ICollection<Person>> GetAll();
? ??
? ? Task<Person> GetPersonById(int personId);
? ??
? ? Task<Person> AddPerson(Person toCreate);

? ? Task<Person> UpdatePerson(int personId, string name, string email);

? ? Task DeletePerson(int personId);
};        

This interface will be used by your API as an abstraction of the repository whose implementation resides in the Infrastructure project.

To create those implementations, create a Repositories folder in the Infrastructure project and add a PersonRepository class:

using Application.Abstractions
using Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace Infrastructure.Repositories;

public class PersonRepository : IPersonRepository
{
? ? private readonly PersonDbContext _context;

? ? public PersonRepository(PersonDbContext context)
? ? {
? ? ? ? _context = context;
? ? }

? ? public async Task<Person> AddPerson(Person toCreate)
? ? {
? ? ? ? _context.Person.Add(toCreate);
? ? ? ? await _context.SaveChangesAsync();

? ? ? ? return toCreate;
? ? }

? ? public async Task DeletePerson(id personId)
? ? {
? ? ? ? var person = _context.Person
? ? ? ? ? ? .FirstOrDefault(p => p.Id == personId);

? ? ? ? if (person is null) return;

? ? ? ? _context.Person.Remove(person);

? ? ? ? await _context.SaveChangesAsync();
? ? }

? ? public async Task<ICollection<Person>> GetAll()
? ? {
? ? ? ? return await _context.Person.ToListAsync();
? ? }

? ? public async Task<Person> GetPersonById(int personId)
? ? {
? ? ? ? return await _context.Person.FirstOrDefaultAsync(p => p.Id == personId);
? ? }

? ? public async Task<Person> UpdatePerson(int personId, string name, string email)
? ? {
? ? ? ? var person = await _context.Person
? ? ? ? ? ? .FirstOrDefaultAsync(p => p.Id == personId);

? ? ? ? person.Name = name;
? ? ? ? person.Email = email;
? ? ?
? ? ? ? await _context.SaveChangesAsync();

? ? ? ? return chapter;
? ? }
};        

Finally, to be able to use the created repository in your command and query handlers, you need to register the service in the WebApi project. In the Program.cs file, add the following code:

builder.Services.AddScoped<IPersonRepository, PersonRepository>();        

Database and Migrations

We successfully implemented a repository pattern, but there is more work to do. For our application to work, it needs a connection to the Database. In this case, we will use MSSQL.

First of all, add your connection string in the appsettings.json file:

"ConnectionStrings": { 
  "Default": "YOUR CONNECTION STRING HERE"
}        

After that, we need to add our PersonDbContext to the Services collection:

var cs = builder.Configuration.GetConnectionString("Default")
builder.Services.AddDbContext<PersonDbContext>(opt => opt.UseSqlServer(cs));;        

For this to work, you need to install the following NuGet packages in our WebApi project:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools        

Since we use the code-first approach, we need to perform a migration to our Database to create the required tables. To perform a migration, go to Package Manager Console, select the Infrastructure project as a default project, and enter the following command:

add-migration InitialMigration        

To commit this migration(to commit changes to the Database), we need to run the following command:

update-database        

Implementing the CQRS Pattern

In CQRS we separate the read and write operations (queries and commands) of an application into separate models that require their handlers. To achieve this, in the Application project, create the following folder structure with the specified classes.

?? Application
└── ?? Person
    ├── ?? Commands
    │   ├── ?? CreatePerson.cs
    │   ├── ?? DeletePerson.cs
    │   └── ?? UpdatePerson.cs
    ├── ?? Queries
    │   ├── ?? GetAllPersons.cs
    │   └── ?? GetPersonById.cs
    ├── ?? CommandHandlers
    │   ├── ?? CreatePersonHandler.cs
    │   ├── ?? DeletePersonHandler.cs
    │   └── ?? UpdatePersonHandler.cs
    └── ?? QueryHandlers
        ├── ?? GetAllPersonsHandler.cs
        └── ?? GetPersonByIdHandler.cs        

Here's a breakdown of the components in our directory structure:

  • Commands: This directory contains the command objects, such as CreatePerson.cs, DeletePerson.cs, and UpdatePerson.cs. These command objects represent the intent to perform specific actions on the Person entity.
  • Queries: This directory contains the query objects, such as GetAllPersons.cs and GetPersonById.cs. These query objects represent the intent to retrieve data from the Person entity.
  • CommandHandlers: This directory contains the command handlers, such as CreatePersonHandler.cs, DeletePersonHandler.cs, and UpdatePersonHandler.cs. These handlers are responsible for executing the corresponding commands and updating the state of the Person entity.
  • QueryHandlers: This directory contains the query handlers, such as GetAllPersonsHandler.cs and GetPersonByIdHandler.cs. These handlers are responsible for handling the queries and returning the requested data from the Person entity.

In the following parts of this article, we will cover the implementation of one query and one command as well as the implementation of their handlers to avoid repetitiveness and to make this article as concise as possible.

Command Implementation

In CQRS, Command represents the intent to perform an action or modify the state of the system. The following is an example of how you can structure a Command in CQRS using MediatR:

using Domain.Entities
using MediatR;

namespace Application.Person.Commands;

public class CreatePerson : IRequest<Person>
{
? ? public string? Name { get; set; }

? ? public string? Email { get; set; }
};        

The code snippet defines a Command called CreatePerson. It represents the intention to create a Person entity. The command has two properties: Name and Email, which are used to provide the necessary information for creating a Person. It implements the IRequest<Person> interface from the MediatR library, indicating that it expects a response of type Person once it is handled.

This command can be sent to a Command Handler, which contains the logic to handle the Command and create the Person entity accordingly. Using the MediatR library simplifies the process of invoking the appropriate Handler and returning the response, if any, after executing the command.

This is the code for CreatePersonHandler:

using Application.Abstractions
using Application.Person.Commands;
using Domain.Entities;
using MediatR;

namespace Application.Person.CommandHandlers;

public class CreatePersonHandler : IRequestHandler<CreatePerson, Person>
{
? ? private readonly IPersonRepository _personRepository;


? ? public CreatePersonHandler(IPersonRepository _personRepository)
? ? {
? ? ? ? _personRepository = personRepository;
? ? }


? ? public async Task<Person> Handle(CreatePerson request, CancellationToken cancellationToken)
? ? {
? ? ? ? var person = new Person
? ? ? ? {
? ? ? ? ? ? Name = request.Title,
? ? ? ? ? ? Email = request.Description
? ? ? ? };

? ? ? ? return await _personRepository.AddPerson(person);
? ? }
};        

Query Implementation

In CQRS, a query represents a request for data or information from the system without causing any changes to the system's state. It is used to retrieve data or perform read-only operations.

Queries can be designed and optimized specifically for read-intensive operations, such as data retrieval, filtering, sorting, and aggregating.

The following code is an example of retrieving the Person by ID:

using Domain.Entities
using MediatR;

namespace Application.Person.Queries;

public class GetPersonById : IRequest<Person>
{
? ? public int Id { get; set; }
};        

A Query Handler is responsible for handling Queries and retrieving data from the system. It receives a Query request, performs the necessary operations to fetch the data, and returns the result to the caller.

The following code is an example of QueryHandler for GetPersonById Query:

using Application.Abstractions
using Application.Person.Queries;
using Domain.Entities;
using MediatR;

namespace Application.Person.QueryHandlers;

public class GetPersonByIdHandler : IRequestHandler<GetPersonById, Person>
{
? ? private readonly IPersonRepository _personRepository;

? ? public GetPersonByIdHandler(IPersonRepository personRepository)
? ? {
? ? ? ? _personRepository = personRepository;
? ? }

? ? public async Task<Person> Handle(GetPersonById request, CancellationToken cancellationToken)
? ? {
? ? ? ? return await _personRepository.GetPersonById(request.Id);
? ? }
};        

This code snippet shows a Query Handler that retrieves a Person by their ID using an IPersonRepository and returns the person as the query result. It implements IRequestHandler from MediatR in the same way the CommandHandlers do.

Usage

In a WebApi project, you can use the Queries and Commands from your CQRS implementation to handle incoming HTTP requests. Here's an example of how you can utilize Queries and Commands in your web API endpoints.

First, in Program.cs, add the following line:

builder.Services.AddMediatR(typeof(CreatePerson));        

By passing typeof(CreatePerson), you are instructing MediatR to scan the assembly where CreatePerson is defined and register all the necessary dependencies for MediatR to work, such as the Command and Query Handlers.

After configuring MediatR, you can start using the IMediator interface to send commands and queries to be handled by the appropriate handlers.

For the usage example, we will use an endpoint for getting a Person by ID:

persons.MapGet("/{id:int}", GetPersonById);        

The following method represents a method GetPersonById used by the "/{id:int}" endpoint:

private async Task<IResult> GetPersonById(IMediator mediator, int id)
{
? ? var getPerson = new GetPersonById { Id = id };
? ? var person = await mediator.Send(getPerson);

? ? return TypedResults.Ok(person);
}        

The method creates a GetPersonById query object with the provided ID and sends it using the mediator.Send method.

It awaits the response from the Query Handler and receives the Person data.

The method wraps the retrieved person in a result object (IResult) using TypedResults.Ok to indicate a successful operation.

By leveraging the MediatR library and your Query Handlers and Command Handlers, you can easily handle different HTTP requests and map them to the corresponding Queries or Commands in your WebApi endpoints. This way, you can maintain a clear separation between handling read (queries) and write (commands) operations in your application.

Conclusion

In conclusion, the combination of Clean Architecture, CQRS, and the Repository Pattern provides developers with a powerful toolkit for building robust and maintainable software systems. By embracing these architectural concepts, developers can create modular, scalable, and adaptable solutions.

Clean Architecture emphasizes the separation of concerns, enabling easier testing and evolution of the system. CQRS promotes the segregation of read and write operations, improving performance and scalability, while the Repository Pattern provides a flexible abstraction layer for data access, enhancing maintainability and testability.

By leveraging these patterns, developers can design software systems that are resilient to change, easier to maintain, and capable of evolving to meet the ever-changing demands.

In the end, it's worth noting that while both patterns offer significant benefits, they also introduce additional complexity to the system. As with any architectural pattern, it's important to carefully consider the specific requirements and characteristics of your application before deciding to adopt these patterns.

Aman Mishra

3K+ @LinkedIn?? | Open for collaboration | 3 Million+ views |(C#, Problem Solving, SQL)@Hackerrank | 400+ Solved @GFG | Backend Code | Technical Content Writer | Data Science enthusiast #interview, #softwaredevelopment

5 个月

Great Work

回复
Tarun Nigam

Technical Lead at Vector Consulting group

6 个月

Which design pattern is better for handling 5 million records by 200-300 users

回复
M A R BIN SIDDIQUI MAMUN

Software Engineering Manager | Fin-Tech Specialist

6 个月

Best article I ever read on CQRS for fundamentals. Thanks Edin ?ahbaz.

回复
Marlo Hutchinson, PMP?, PMI-RMP?, CC, CL

Senior Software Engineer at Valor Healthcare

7 个月

Thank you!

回复
Charles Chinoziem-Best

Senior Software Engineer l C# .Net core | JavaScript | Typescript | React | React Native | NodeJs | NextJs.

8 个月

How do you factor authentication? I'm having difficult deciding what the best approach is, Let's say as you have Person in the domain layer, the person needs to be authenticated to use the application, I want to use Identity which has to be in the infrastructure layer to no violate clean architecture, I'm having issues maintaining one to one relationship between the Person model and the IdentityUser... any thoughts on this?

回复

要查看或添加评论,请登录

社区洞察

其他会员也浏览了