Nel mondo dello sviluppo software, creare applicazioni che siano facili da mantenere, scalabili ed efficienti è una priorità assoluta. Per raggiungere questo obiettivo, gli sviluppatori utilizzano vari modelli di progettazione e librerie. Una combinazione che ha guadagnato popolarità è il modello CQRS (Command Query Responsibility Segregation) con MediatR in ASP.NET Core, utilizzando il linguaggio di programmazione C#.
Introduzione CQRS e MediatR
CQRS (Command Query Responsibility Segregation) e MediatR sono due concetti chiave nell’ecosistema di sviluppo ASP.NET Core.
CQRS
Il CQRS, o Command Query Responsibility Segregation, è un modello architetturale che separa il modello di dominio in due parti distinte: una per la scrittura dei dati (comandi) e l’altra per la lettura dei dati (query). Questa separazione consente una maggiore flessibilità nel design dell’applicazione e una migliore gestione delle richieste.
MediatR
MediatR è una libreria che semplifica l’implementazione del pattern CQRS, fornendo un sistema di mediatori che facilita la distribuzione dei comandi e delle query all’interno dell’applicazione.
Se preferisci vedere subito il codice completo, puoi trovare il progetto su GitHub: CQRSExample.NET
Implementazione
Creazione progetto
dotnet new webapi -n CQRSExample.NET
cd CQRSExample.NET
Una volta creato il progetto, procedi con l’installazione del pacchetto NuGet di MediatR e la sua configurazione.
Installazione MediatR
Per installare il pacchetto di MediatR, è possibile eseguire il seguente comando oppure navigare nella gestione dei pacchetti NuGet.
dotnet add package MediatR
Configurazione MediatR
Prima di procedere con la configurazione del servizio, è importante controllare la versione installata di MediatR. In questo articolo è stata utilizzata la versione 12, quindi è possibile procedere con la registrazione utilizzando:
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()));
Per le versioni precedenti alla 12, è necessario registrare MediatR utilizzando:
builder.Services.AddMediatR(typeof(Startup).Assembly);
Configurazione funzionalità
Per organizzare al meglio il progetto, è utile suddividere le funzionalità in ‘features’, creando una struttura ordinata delle cartelle, ad esempio ‘Features -> NomeFeature’. All’interno di ciascuna feature, verrà definito sia il comando/query che il relativo handler. Per emulare un database, sarà necessario creare una classe statica che restituirà la lista degli utenti e consentirà di aggiungerne di nuovi. È importante notare che i dati inseriti nella classe statica verranno persi ogni volta che l’applicazione viene interrotta.
FakeDatabase.cs
namespace CQRSExample.NET.Database;
public static class FakeDatabase
{
public static List<string> UserNames =
[
"User123",
"TestUser",
"GuestUser",
"DemoUser",
"SampleUser"
];
}
Funzionalità di lettura
GetUsernameQuery.cs
using MediatR;
namespace CQRSExample.NET.Features.GetUsernames;
public class GetUsernameQuery : IRequest<List<string>>;
Dopo aver definito la query, è necessario implementare la logica per la lettura degli utenti. Successivamente, sarà necessario assocerai la query all’handler.
GetUserNameHandler.cs
using CQRSExample.NET.Database;
using MediatR;
namespace CQRSExample.NET.Features.GetUserName;
public class GetUserNameHandler : IRequestHandler<GetUserNameQuery>
{
public Task Handle(GetUserNameQuery request, CancellationToken cancellationToken)
{
return Task.FromResult(FakeDatabase.UserNames);
}
}
Feature aggiungi nome utente
Ora che la funzionalità di lettura è stata creata, è possibile procedere con la definizione della feature che gestirà l’aggiunta di nuovi utenti nel database temporaneo.
AddUserNameCommand.cs
using MediatR;
namespace CQRSExample.NET.Features.AddUserName;
public class AddUserNameCommand : IRequest<List<string>>
{
public string UserName { get; set; }
}
Come nel passaggio precedente della lettura degli utenti, una volta definito il command per l’aggiunta di nuovi utenti, si può procedere con l’implementazione della logica nel relativo handler.
using CQRSExample.NET.Database;
using MediatR;
namespace CQRSExample.NET.Features.AddUserName;
public class AddUserNameHandler : IRequestHandler<AddUserNameCommand, List<string>>
{
public Task<List<string>> Handle(AddUserNameCommand request, CancellationToken cancellationToken)
{
FakeDatabase.UserNames.Add(request.UserName);
return Task.FromResult(FakeDatabase.UserNames);
}
}
Creazione API
Adesso che le feature sono state implementate, si può procedere con l’implementazione di un controller per gestire le richieste HTTP e verificare il corretto funzionamento delle feature utilizzando Swagger UI.
using CQRSExample.NET.Features.AddUserName;
using CQRSExample.NET.Features.GetUserName;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace CQRSExample.NET.Controllers;
[Route("/[controller]/[action]")]
public class UserNameController(ISender sender) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> List()
{
return Ok(await sender.Send(new GetUserNameQuery()));
}
[HttpPost]
public async Task<IActionResult> Add(string username)
{
return Ok(await sender.Send(new AddUserNameCommand
{
UserName = username
}));
}
}
Ricordo che a partire da ASP.NET Core 8, la registrazione del servizio dei controller e la loro mappatura non viene più implementata di default. Di conseguenza, è necessario configurare lo startup dell’applicazione andando in Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddControllers(); // Registrazione del servizio che si occupa dei controllers
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers(); // Mappatura dei controllers
app.Run();
Ora si può avviare l’applicazione e provare le API utilizzando Swagger.
Conclusione
Separando le logiche di lettura da quelle di scrittura, è possibile ottimizzare le performance e migliorare la manutenibilità del codice. Tuttavia, è importante considerare la complessità aggiuntiva che CQRS introduce, poiché richiede una gestione separata dei dati e delle operazioni, il che può rendere più difficile l’implementazione e la gestione iniziale. In definitiva, CQRS è uno strumento potente, ma va utilizzato con consapevolezza e in contesti che giustifichino il suo impiego.