In this article, we will learn about 'Clean Architecture', 'CQRS Pattern With MediatR' by implementing in .NET7 Web API.
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).
Let's first create new folders like 'Beach/GetAllBeaches'.
Let's create a response model like 'GetAllBeachesResponse.cs' in 'Beach/GetAllBeaches'(folder).
Let's create a mapper profile class like 'GetAllBeachesMapper.cs' in 'Beach/GetAllBeaches'(folder).
Clean Architecture:
The Clean Architecture comprises of:- Domain Layer
- Application Layer
- Infrastructure Layer
- UI Layer
Domain Layer: the domain layer project will have 'Entities'(Domain Models), 'Domain Services', and 'Value Object'. This project is a parent project and it is independent on all other projects.
Application Layer: the application layer project contains business logic files, DTOs, mapper, etc. This project can depend only on the 'Domain Layer Project'.
Infrastructure Layer: the infrastructure layer project contains database logic, external API calls, emails, logging, etc. This project can depend on 'Domain Layer' & 'Application Layer'.
UI Layer: the UI layer project like 'MVC', 'Razor Page', 'Web API', etc. The 'UI Layer' should only consume the 'Application Layer'. But we will reference 'Infrastructure Layer' into the 'UI Layer' only for dependency injection layer.
Create Projects:
Let's create projects like below:
- Dot7.Architecture.Domain(class library)
- Dot7.Architecture.Application(class library)
- Dot7.Architecture.Infrastructure(class library)
- Dot7.Architecture.Api(Web API)
Add Project References:
Now we have to add the references to the projects as below:
(Step 1)
The 'Dot7.Architecture.Domain' Project is independent projects
(Step 2)
The 'Dot7.Architecture.Application' project depends on 'Dot7.Architecture.Domain'.
(Step 3)
The 'Dot7.Architecture.Infrstructure' project depends on 'Dot7.Architecture.Domain' & 'Dot7.Architecture.Application'.
(Step 4)
The 'Dot7.Architecture.Api' project depends on 'Dot7.Architecture.Application' & 'Dot7.Architecture.Infrastucture'.
Install Microsoft DI Package & Create DI Service Registration Files In The Required Projects:
Let's install the 'Microsoft.Extensions.DependencyInjection' & 'Microsoft.Extensions.Configuration' packages into 'Dot7.Architecture.Infrastructure' and 'Dot7.Architecture.Application'.
Visual Studio 2022:
Install-Package Microsoft.Extensions.DependencyInjection -Version 7.0.0
Install-Package Microsoft.Extensions.Configuration -Version 7.0.0
Visual Studio Code:
dotnet add package Microsoft.Extensions.DependencyInjection --version 7.0.0
dotnet add package Microsoft.Extensions.Configuration --version 7.0.0
In 'Dot7.Architecture.Infrastructure' project let's create a new file like 'RegisterService.cs'.
Dot7.Architecture.Infrastructure/RegisterService.cs:
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dot7.Architecture.Infrastructure; public static class RegisterService { public static void ConfigureInfraStructure(this IServiceCollection services, IConfiguration configuration) { } }
- Here we can register our DI services.
Dot7.Architecture.Application/RegisterService.cs:
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dot7.Architecture.Application; public static class RegisterService { public static void ConfigureApplication(this IServiceCollection services, IConfiguration configuration) { } }
- Here we can register our DI services related to 'Dot7.Architecture.Application' project.
Dot7.Architecture.Api/Program.cs:
using Dot7.Architecture.Application; using Dot7.Architecture.Infrastructure; builder.Services.ConfigureInfraStructure(builder.Configuration); builder.Services.ConfigureApplication(builder.Configuration);
Sample Table Script:
Let's create a sample table like 'Beach'.
CREATE TABLE Beach( Id int IDENTITY (1,1) NOT NULL, BeachName varchar (200) , Place varchar (200) , ImageUrl varchar(1000) CONSTRAINT PK_Employee_Id PRIMARY KEY (Id) )
Add 'Beach' Entity In Domain Layer Project:
Let's add the 'Beach.cs' entity in the 'Dot7.Architecture.Domain' project. So create a folder like 'Entities' and add inside of it.
Dot7.Architecture.Domain/Entities/Beach.cs:
namespace Dot7.Architecture.Domain.Entities; public class Beach { public int Id{get;set;} public string? BeachName{get;set;} public string? Place{get;set;} public string? ImageUrl{get;set;} }
Install Entity FramworkCore NuGet Package In Required Projects:
Let's install the following NuGet packages into the 'Dot7.Architecture.Application' & 'Dot7.Architecture.Infrastructure' projects.
Visual Studio 2022:
Install-Package Microsoft.EntityFrameworkCore -Version 7.0.4
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 7.0.4
Visual Studio Code:
dotnet add package Microsoft.EntityFrameworkCore --version 7.0.4
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 7.0.4
Configure DatabaseContext:
First, let's create an interface for the database context like 'IMyWorldDbContext.cs'. So this interface must be created in 'Dot7.Architecture.Application' project.
Dot7.Architecture.Application/Context/IMyWorldDbContext.cs:
using Microsoft.EntityFrameworkCore; namespace Dot7.Architecture.Application.Context; public interface IMyWorldDbContext { DbSet<Dot7.Architecture.Domain.Entities.Beach> Beach{get;} Task<int> SaveToDbAsync(); }
- (Line: 6) Registered our table entity.
- (Line: 8) The 'SaveToDbAsync' method definition is defined.
Dot7.Architecture.Infrastructure/Context/MyWorldDbContext.cs:
using Dot7.Architecture.Application.Context; using Dot7.Architecture.Domain.Entities; using Microsoft.EntityFrameworkCore; namespace Dot7.Architecture.Infrastructure.Context; public class MyWorldDbContext : DbContext, IMyWorldDbContext { public MyWorldDbContext(DbContextOptions<MyWorldDbContext> options):base(options) { } public DbSet<Beach> Beach {get;set;} public async Task<int> SaveToDbAsync() { return await SaveChangesAsync(); } }
- (Line: 11) Here we implemented our 'Beach' entity property.
- (Line: 12-15) In 'SaveToDbAsync' method we invoke our database context method i.e, 'SaveChangesAsync'.
Dot7.Architecture.Api/appsettings.Development.json:
"ConnectionStrings": { "MyWorldDbConnection":"" }Now let's register our 'MyWorldDbContext' service in 'Dot7.Architecture.Infrastructure' project.
Dot7.Architecture.Infrastructure/RegisterServices.cs:
using Dot7.Architecture.Application.Context; using Dot7.Architecture.Infrastructure.Context; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dot7.Architecture.Infrastructure; public static class RegisterService { public static void ConfigureInfraStructure(this IServiceCollection services, IConfiguration configuration) { services.AddDbContext<MyWorldDbContext>(options => { options.UseSqlServer(configuration.GetConnectionString("MyWorldDbConnection")); }); services.AddScoped<IMyWorldDbContext>(option => { return option.GetService<MyWorldDbContext>(); }); } }
- (Line: 12-15) Here registered our 'MyWorldDoContext' with the SQL connection string.
- (Line: 16-18) Here 'IMyWorldDbContext' , and 'MyWorldDbContext' are at the scope level.
CQRS Pattern:
CQRS stands for Command Query Responsibility Segregation. CQRS guides us to separate our logical implementations into 2 categories like 'Commands' and 'Query'. The 'Commands' specifies operations like 'create' or 'update' in the database. The 'Query' specifies the operation to fetch the data.
In CQRS models(Request/Response classes) are independent or owned by a single operation, which means model classes can't be shared between the different 'Commands' or different 'Queries' or between a 'Command' and 'Query'.
From the diagram one thing to observe Request/Response model(optional), that's because sometime we will use query parameters or return a simple scalar type in those cases we won't create models.MediatR:
MediatR is another design pattern. MediatR builds with handlers that have the capability to work on Commands and Queries.
MediatR reduces a lot of boilerplate code like injecting multiple services into the controller. MediatR provides a single entry point that expects a RequestModel, so based on the RequestModel corresponding handler gets invoked. It works as a centralized communication hub because from the controller any handler gets invoked only through it. So it's the most common recommendation to use MediatR with the CQRS pattern.
From the diagram, we have to understand MediatR should have both RequestModel and ResponseModel. In MediatR 'RequestModel' always be a user-defined class whereas 'ResponseModel' is either a user-defined class or scalar type.Install MediatR Package:
Now install the MediatR package in 'Dot7.Architecture.Application' and 'Dot7.Architecture.Api' projects.
Visual Studio 2022:
Install-Package MediatR -Version 12.0.0
Visual Studio Code:
dotnet add package MediatR --version 12.0.0
Register MediatR Service:
Let's register the MediatR service in 'Dot7.Architecture.Application'.
Dot7.Architecture.Application/RegisterService.cs:
using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dot7.Architecture.Application; public static class RegisterService { public static void ConfigureApplication(this IServiceCollection services, IConfiguration configuration) { services.AddMediatR(_ => _.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); } }
- Here we registered the assembly name with 'MediatR' so that it can read all the files related to MediatR
Install AutoMapper Package:
Let's install the AutoMapper package in the 'Dot7.Architecture.Application' project.
Visual Studio 2022:
Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection -Version 12.0.0
Visual Studio Code:
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection --version 12.0.0
Register AutoMapper Service:
Let's register the AutoMapper service in 'Dot7.Architecture.Application' project.
Dot7.Architecture.Application/RegisterService.cs:
services.AddAutoMapper(Assembly.GetExecutingAssembly());
Implement CQRS Query Handler To Fetch Data:
Let's implement CQRS query handler to fetch data from the database. Our CQRS files will be added under our 'Dot7.Architectur.Application' project.
We have to create CQRS files as below
Dot7.Archtiecture.Application/Beach/GetAllBeaches/GetAllBeachesResponse.cs:
namespace Dot7.Architecture.Application.Beach.GetAllBeaches; public class GetAllBeachesResponse { public int Id{get;set;} public string? BeachName{get;set;} public string? Place{get;set;} public string? ImageUrl{get;set;} }
- Here is our response model.
Let's create a request model like 'GetAllBeachesRequest.cs' in 'Beach/GetAllBeaches'(folder).
Dot7.Architecture.Application/Beach/GetAllBeaches/GetAllBeachesRequest.cs:
using MediatR; namespace Dot7.Architecture.Application.Beach.GetAllBeaches; public class GetAllBeachesRequest:IRequest<List<GetAllBeachesResponse>> { }
- In general request object mostly contains query properties, pagination properties, etc. In my case, we don't have any properties, but to use MediatR CQRS we must contain a request object.
- (Line: 3) Inherit the 'MediatR.IRequest<TResponse>'. Here TResponse is our response object which means 'List<GetAllBeachResponse>'.
Dot7.Architecture.Application/Beach/GetAllBeaches/GetAllBeachesMapper.cs:
using AutoMapper; namespace Dot7.Architecture.Application.Beach.GetAllBeaches; public class GetAllBeachesMapper:Profile { public GetAllBeachesMapper() { CreateMap<Dot7.Architecture.Domain.Entities.Beach,GetAllBeachesResponse>(); } }
- Here are source object is the 'Beach' entity and the destination object is 'GetAllBeachesResponse'.
Dot7.Architecture.Application/Beach/GetAllBeaches/GetAllBeachesQueryHandler.cs:
using AutoMapper; using AutoMapper.QueryableExtensions; using Dot7.Architecture.Application.Context; using MediatR; using Microsoft.EntityFrameworkCore; namespace Dot7.Architecture.Application.Beach.GetAllBeaches; public class GetAllBeachesQueryHandler : IRequestHandler<GetAllBeachesRequest, List<GetAllBeachesResponse>> { private readonly IMyWorldDbContext _myWorldDbContext; private readonly IMapper _mapper; public GetAllBeachesQueryHandler(IMyWorldDbContext myWorldDbContext, IMapper mapper) { _myWorldDbContext = myWorldDbContext; _mapper = mapper; } public Task<List<GetAllBeachesResponse>> Handle(GetAllBeachesRequest request, CancellationToken cancellationToken) { return _myWorldDbContext.Beach.ProjectTo<GetAllBeachesResponse>(_mapper.ConfigurationProvider) .ToListAsync(); } }
- (Line: 7) Here our handler inherits 'MediatR.IRequestHandler<TRequest,TResponse>'. The 'TRequest' is our 'GetAllBeachRequest' and 'TResponse' is our 'GetAllBeachResponse' collection.
- (Line: 9&16) Injected the 'IMyWorldDbContext' and 'IMapper'.
- (Line: 17-21) Here inside of the 'Handle' method fetches all the data from the database. Here we use 'ProjecTo' mapper method.
Dot7.CleanArchitecture.Api/Controllers/BeachController.cs:
using Dot7.Architecture.Application.Beach.GetAllBeaches; using MediatR; using Microsoft.AspNetCore.Mvc; namespace Dot7.Architecture.Api.Controllers; [ApiController] [Route("[controller]")] public class BeachController:ControllerBase { private readonly IMediator _mediator; public BeachController(IMediator mediator) { _mediator = mediator; } [HttpGet] public async Task<IActionResult> GetAsync() { var response = await _mediator.Send(new GetAllBeachesRequest()); return Ok(response); } }
- (Line: 9-13) Here injected 'MediatR.IMediator' service.
- (Line: 17) To the 'IMediator.Send()' method we have to pass a request object like 'GetAllBeachesRequest'.
Implement CQRS Command Handler To Save Data:
Let's implement CQRS command handler to save the data into the database.
We have to create our CQRS files as below:
Let's create the request object 'CreateBeachRequest.cs' in the 'Beach/CreateBeach' folder.
Dot7.Architecture.Application/Beach/CreateBeach/CreateBeachRequest.cs:
Dot7.Architecture.Application/Beach/CreateBeach/CreateBeachRequest.cs:
using MediatR; namespace Dot7.Architecture.Application.Beach.CreateBeach; public class CreateBeachRequest:IRequest<int> { public string? BeachName{get;set;} public string? Place{get;set;} public string? ImageUrl{get;set;} }
- The 'CreateBeachRequest' class will be our payload object.
- (Line: 3) Inheriting the 'MediatR.IRequest<TResponse>', here our TResponse is 'int'.
Dot7.Architecture.Application/Beach/CreateBeach/CreateBeachMapper.cs:
using AutoMapper; namespace Dot7.Architecture.Application.Beach.CreateBeach; public class CreateBeachMapper:Profile { public CreateBeachMapper() { CreateMap<CreateBeachRequest, Dot7.Architecture.Domain.Entities.Beach>(); } }
- Here creating a mapping between 'CreateBeachRequest'(source object) and 'Beach'(destination object).
Dot7.Architecture.Application/Beach/CreateBeach/CreateBeachCommandHandler.cs:
using AutoMapper; using Dot7.Architecture.Application.Context; using MediatR; namespace Dot7.Architecture.Application.Beach.CreateBeach; public class CreateBeachCommandHandler : IRequestHandler<CreateBeachRequest, int> { private readonly IMyWorldDbContext _myWorldDbContext; private readonly IMapper _mapper; public CreateBeachCommandHandler(IMyWorldDbContext myWorldDbContext, IMapper mapper) { _myWorldDbContext = myWorldDbContext; _mapper = mapper; } public async Task<int> Handle(CreateBeachRequest request, CancellationToken cancellationToken) { var newBeach = _mapper.Map<Dot7.Architecture.Domain.Entities.Beach>(request); _myWorldDbContext.Beach.Add(newBeach); await _myWorldDbContext.SaveToDbAsync(); return newBeach.Id; } }
- (Line: 5) Here command handler inherits 'MediatR.IRequestHandler<TRequest,TResponse>'. The 'TRequest' is 'CreateBeachRequest' and 'TResponse' is 'int'.
- (Line: 7-14) Injected 'IMyWorldDbContext' and 'IMapper' services.
- (Line: 17) Mapping our payload type 'CreateBeachRequest' to our table entity 'Beach'.
- (Line: 18&19) Saving the item to the database.
- (Line:20) Return the newly created item id as a response.
Dot7.CleanArchitecture.API/Controller/BeachController.cs:
[HttpPost] public async Task<IActionResult> PostAsync(CreateBeachRequest payload) { var newlyCreateItemId = await _mediator.Send(payload); return Ok(newlyCreateItemId); }
- To 'IMediator.Send()' method we pass our 'CreateBeachRequest' model as an input parameter.
(Step 2)
MediatR IPipelineBehavior:
The 'IPipelineBehavior' exactly works like Asp.Net core middleware, but its starting execution and ending execution happen within the 'IMediator'. So the 'IMediator.Send()' is usually used to invoke the 'Query' or 'Command' handlers. So if we implement 'IPipelineBehavior' then begin logic inside of its start executes then invokes 'Query' or 'Command' handlers, later again go through 'IPipelineBehavior' and executes the end logic.Install FluentValidation NuGet Package:
Let's install the FluentValidation NuGet package in 'Dot7.Architecture.Application' project.
Visual Studio 2022:
Install-Package FluentValidation.DependencyInjectionExtensions -Version 11.5.1
Visual Studio Code:
dotnet add package FluentValidation.DependencyInjectionExtensions --version 11.5.1
Configure FluentValidation Service:
Let's register the FluentValidtion service in 'Dot7.Architecture.Application' project.
Dot7.Architecture.Application/RegisterService.cs:
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
Implement MediatR PipelineBehavior Validation:
Let's create a folder like 'Common' in 'Dot7.Architecture.Application'.
Now let's create a new PiplineBehavior class like 'ValidatorBehavior.cs' in the 'Common' folder.
Dot7.Architecture.Application/Common/ValidatorBehavior.cs:
using FluentValidation; using MediatR; namespace Dot7.Architecture.Application.Common; public class ValidatorBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IEnumerable<IValidator<TRequest>> _validators; public ValidatorBehavior(IEnumerable<IValidator<TRequest>> validators) { _validators = validators; } public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { if (_validators.Any()) { var context = new ValidationContext<TRequest>(request); var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); if (failures.Count != 0) throw new FluentValidation.ValidationException(failures); } return await next(); } }
- The role of 'ValidatorBehavior' class is to validate the validation rules that we going to apply on the API payload.
- (Line: 5) Inherits the 'MediatR.IPipelineBehavior<TRequest, TResponse>'.
- (Line: 7-11) Injected the 'IEnumerable<IValidator<TRequest>>'.
- (Line: 12-23) Inside of the 'Handle' method we implement logic to validate the request object. If any error found validation error will be thrown without any further execution of the request.
Dot7.Architecture.Application/RegisterService.cs:
using System.Reflection; using Dot7.Architecture.Application.Common; using FluentValidation; using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dot7.Architecture.Application; public static class RegisterService { public static void ConfigureApplication(this IServiceCollection services, IConfiguration configuration) { services.AddMediatR(_ => _.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddAutoMapper(Assembly.GetExecutingAssembly()); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorBehavior<,>)); } }
- (Line: 18) Registered our 'ValidatorBehavior' class with the 'IPipelineBehavior'.
Apply Validation On 'CreateBeachRequest':
Let's add validation rules for 'CreateBeachRequest' entity by creating a new class like 'CreateBeachValidtor.cs' in 'Beach/CreateBech' folder.
Dot7.Architecture.Application/Beach/CreateBeach/CreateBeachValidator.cs:
using FluentValidation; namespace Dot7.Architecture.Application.Beach.CreateBeach; public class CreateBeachValidator : AbstractValidator<CreateBeachRequest> { public CreateBeachValidator() { RuleFor(x => x.BeachName).NotEmpty(); } }
- Here using fluent validation we applied a rule on 'CreateBeachRequest' object i.e 'BeachName' can't be an empty value.
(Step 2)
Support Me!
Buy Me A Coffee
PayPal Me
Video Session:
Wrapping Up:
Hopefully, I think this article delivered some useful information on the .NET 7 API using Clean Architecture. using I love to have your feedback, suggestions, and better techniques in the comment section below.
Great article, Naveen! Thank you!
ReplyDeleteIt’s an attractive pattern, use it informally all the time but I can’t help but think some bit of source generation would make it more palatable. Hand coding all the boilerplate takes alot of the time you should be writing the queries, etc.
ReplyDelete