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.

Tagged in:

,