Mediator e MediatR in C#: separare richieste e handler

Come MediatR separa chi invia una richiesta da chi la esegue. Un esempio con command e query, i limiti del pattern e quando conviene introdurlo.

Quando un controller coordina molte operazioni, diventa difficile capire dove si trova la logica di ciascuna richiesta. Con MediatR puoi rappresentare un'operazione con un messaggio e affidarne la gestione a un handler. Il chiamante conosce la richiesta e il risultato atteso, senza dipendere direttamente dalla classe che la esegue.

Cos'è il pattern Mediator

È un design pattern che riduce le comunicazioni dirette fra oggetti. Invece di avere più classi che si chiamano fra loro, introduci un mediatore che smista le richieste. Gli handler mantengono le dipendenze necessarie al proprio lavoro: repository, servizi esterni o altre classi applicative.

Cosa ti dà

  • Accoppiamento debole tra le classi
  • Un handler riconoscibile per ciascuna richiesta
  • Separazione tra chi invia la richiesta e chi la gestisce

Quando ha senso usarlo

Valuta il pattern quando:

  • Più classi che devono parlarsi senza conoscersi a vicenda
  • Vuoi spezzare le dipendenze strette
  • Le interazioni tra classi cambiano spesso

In un CRUD piccolo, chiamare direttamente un servizio può essere più facile da seguire. MediatR aggiunge messaggi e handler: conviene quando questa separazione rende le operazioni più riconoscibili. Le dipendenze circolari e le responsabilità confuse vanno comunque risolte nel progetto delle classi.

Implementazione con MediatR

Il codice qui sotto definisce due tipi di richieste con MediatR: un comando per aggiornare il nome utente, una query per leggerlo. Avere comandi e query separati è un modo per distinguere letture e scritture, coerente con CQRS (Command Query Responsibility Segregation). Questo esempio mostra soltanto la separazione delle richieste: non introduce database separati o scalabilità indipendente.

using MediatR;

public class UpdateUserNameCommand : IRequest<bool>
{
    public Guid UserId { get; }
    public string NewUserName { get; }

    public UpdateUserNameCommand(Guid userId, string newUserName)
    {
        UserId = userId;
        NewUserName = newUserName;
    }
}

public class GetUserNameQuery : IRequest<string>
{
    public Guid UserId { get; }

    public GetUserNameQuery(Guid userId)
    {
        UserId = userId;
    }
}

Questi handler mostrano lo smistamento delle richieste. Scrivono un log e restituiscono valori di esempio: non aggiornano né leggono un utente da un database.

using MediatR;
using Microsoft.Extensions.Logging;

public class UpdateUserNameCommandHandler : IRequestHandler<UpdateUserNameCommand, bool>
{
    private readonly ILogger<UpdateUserNameCommandHandler> _logger;

    public UpdateUserNameCommandHandler(ILogger<UpdateUserNameCommandHandler> logger)
    {
        _logger = logger;
    }

    public async Task<bool> Handle(UpdateUserNameCommand request, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"Updating user {request.UserId} to {request.NewUserName}");
        return true; // Simulazione di successo
    }
}

Handler della Query

public class GetUserNameQueryHandler : IRequestHandler<GetUserNameQuery, string>
{
    private readonly ILogger<GetUserNameQueryHandler> _logger;

    public GetUserNameQueryHandler(ILogger<GetUserNameQueryHandler> logger)
    {
        _logger = logger;
    }

    public async Task<string> Handle(GetUserNameQuery request, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"Fetching user name for user {request.UserId}");
        return "User Name"; // Simulazione di un nome utente
    }
}

MediatR gestisce messaggi all'interno del processo .NET. Il suo README documenta la registrazione degli handler e la configurazione richiesta dalla versione utilizzata.

Un handler si può testare direttamente, passandogli le dipendenze necessarie. Se contiene troppe responsabilità, il mediatore non le elimina. Prima di aggiungere il pattern, prova a isolare una sola operazione e verifica se il percorso dal controller alla logica diventa più chiaro.