Le eccezioni in C# (e in molti altri linguaggi) sono fatte per scenari eccezionali, ma nella vita reale finiscono per gestire la metà del flusso normale: login fallito, validazione, dato non trovato. Il result pattern tratta gli errori come dati, non come interruzioni: il flusso resta esplicito, le performance migliorano, e il codice diventa più facile da leggere a sei mesi di distanza.
Implementazione
Esempio: aggiornare un'attività TODO. Con il result pattern modelliamo successo e fallimento come due strade dello stesso tipo di ritorno.
Il codice sotto è didattico. In produzione conviene quasi sempre usare una libreria battuta, fra cui:
public class Result<T>
{
public bool IsSuccess { get; }
public T Value { get; }
public Error Error { get; }
protected Result(bool isSuccess, T value, Error error)
{
IsSuccess = isSuccess;
Value = value;
Error = error ?? Error.None;
}
public static Result<T> Success(T value) => new Result<T>(true, value, Error.None);
public static Result<T> Failure(Error error) => new Result<T>(false, default, error);
// Conversione implicita da T a Result<T> (successo)
public static implicit operator Result<T>(T value) => Success(value);
// Conversione implicita da Error a Result<T> (fallimento)
public static implicit operator Result<T>(Error error) => Failure(error);
}// Rappresentazione errore
public sealed record Error(string Code, string Description)
{
public static readonly Error None = new(string.Empty, string.Empty);
public override string ToString() => $"{Code}: {Description}";
}Ora vediamo l'uso concreto, accoppiato al Mediator pattern per separare la logica di business dall'infrastruttura ed evitare le referenze circolari.
public class UpdateToDoCommand : IRequest<Result<ToDoItem>>
{
public int Id { get; set; }
public string NewTitle { get; set; }
public UpdateToDoCommand(int id, string newTitle)
{
Id = id;
NewTitle = newTitle;
}
}public class UpdateToDoCommandHandler : IRequestHandler<UpdateToDoCommand, Result<ToDoItem>>
{
private readonly IToDoRepository _repository;
public UpdateToDoCommandHandler(IToDoRepository repository)
{
_repository = repository;
}
public async Task<Result<ToDoItem>> Handle(UpdateToDoCommand request, CancellationToken cancellationToken)
{
var toDoItem = await _repository.GetByIdAsync(request.Id);
if (toDoItem == null)
{
//return Result<ToDoItem>.Failure(new Error("NOT_FOUND", "Item not found"));
return new Error("NOT_FOUND", "Item not found");
}
toDoItem.UpdateTitle(request.NewTitle);
await _repository.UpdateAsync(toDoItem);
return toDoItem;
}
} L'handler usa il generico Result<T>: se l'item non esiste torna un fallimento con codice e messaggio, altrimenti aggiorna e torna l'item. Niente throw, niente try/catch, niente stack trace per casi prevedibili.
Notare che non serve istanziare il tipo di ritorno con Result<ToDoItem>.Failure o Result<ToDoItem>.Success: la conversione implicita definita sopra fa il lavoro. Meno codice, meno rumore.
Dove gestire il risultato
Il consiglio: tieni il Result Pattern nello strato vicino all'utente. Nel Controller, ad esempio, sai cosa farne: se è successo, 200 con il payload; se è fallimento, 4xx col messaggio.
// ...
public class ToDoController : ControllerBase
{
// ...
[HttpPost]
public async Task<ActionResult> JustDoIt()
{
var command = new UpdateToDoCommand(1, "Nuovo Titolo");
var result = await _mediator.Send(command);
if (result.IsSuccess)
{
return Ok(result.Value.Title);
}
else
{
return BadRequest(result.Error);
}
}
}Risultato: niente try/catch nel flusso principale, controllo esplicito su successo e fallimento, e leggibilità migliorata. Chi legge il codice in futuro vede subito cosa può andare storto e dove.
Result o Exception? Quando usare cosa
Il Result Pattern è il vestito giusto per i fallimenti prevedibili: login con password sbagliata, item inesistente, validazione fallita. Sono casi attesi, parte del normale ciclo della tua applicazione.
Le eccezioni servono per i casi imprevisti: connessione al DB caduta, file mancante, configurazione corrotta. Lì throw è legittimo perché l'errore non fa parte del flusso normale e probabilmente nessuno sa cosa fare oltre a fermarsi e loggare.
La regola in due righe: se il codice chiamante deve gestire il caso, è un Result. Se nessuno può gestirlo realmente sul posto, è un'Exception.
In sostanza
Gli errori sono per gli utenti, mentre le eccezioni sono per gli sviluppatori.


