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:
Add references
To implement Clean Architecture, add references to the projects.
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.
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.
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.
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:
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:
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:
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.
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
Technical Lead at Vector Consulting group
6 个月Which design pattern is better for handling 5 million records by 200-300 users
Software Engineering Manager | Fin-Tech Specialist
6 个月Best article I ever read on CQRS for fundamentals. Thanks Edin ?ahbaz.
Senior Software Engineer at Valor Healthcare
7 个月Thank you!
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?