← todos os artigos

C#

Usando o Result Pattern em C# em vez de retornar null

O Result Pattern torna explícitos os estados esperados de sucesso e falha, oferecendo uma alternativa mais clara a retornar null ou lançar exceções para resultados rotineiros de negócio.

Aprendi o Result Pattern enquanto trabalhava com .NET, e ele mudou minha forma de pensar sobre métodos que podem falhar. Em vez de retornar null e deixar que o chamador interprete o que isso significa, o método retorna um objeto que descreve sucesso ou falha.

Isso não significa que todos os valores anuláveis ou todas as exceções devam desaparecer. O padrão é mais útil quando a falha é um resultado esperado e o chamador precisa entender o que aconteceu.

A limitação de retornar null

Considere um método de repositório que retorna null quando não consegue encontrar um usuário. O chamador pode verificar se o valor é null, mas isso não explica o motivo. O identificador era inválido? O usuário não foi encontrado? Alguma outra validação falhou?

Os tipos de referência anuláveis melhoram essa situação ao permitir expressar se uma referência deve aceitar null. O compilador C# usa então análise estática para alertar sobre possíveis atribuições e desreferenciamentos de null. No entanto, esse é um recurso de tempo de compilação; ele não cria um tipo separado em runtime nem adiciona validação em runtime. (learn.microsoft.com)

Um Result<T> acrescenta significado ao resultado da operação. Em vez de retornar User?, um método pode retornar Result<User> contendo o usuário ou um erro específico.

Representando erros

A primeira parte é um pequeno tipo de erro:

public record Error(string Code, string Message)
{
    public static Error None = new(string.Empty, string.Empty);
    public static Error NullValue = new("Error.NullValue", "Um valor nulo foi fornecido.");
}

O código fornece um identificador estável para decisões programáticas e uma mensagem que pode ser registrada em log ou exibida. Error.None representa uma operação bem-sucedida, na qual não existe erro.

Criando o tipo Result

O Result não genérico representa operações que são concluídas com sucesso sem retornar um valor:

public class Result
{
    protected Result(bool isSuccess, Error error)
    {
        switch (isSuccess)
        {
            case true when error != Error.None:
                throw new InvalidOperationException();

            case false when error == Error.None:
                throw new InvalidOperationException();

            default:
                IsSuccess = isSuccess;
                Error = error;
                break;
        }
    }

    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public Error Error { get; }

    public static Result Success() => new(true, Error.None);
    public static Result Failure(Error error) => new(false, error);

    public static Result<T> Success<T>(T value) => new(value, true, Error.None);
    public static Result<T> Failure<T>(Error error) => new(default, false, error);

    public static Result<T> Create<T>(T? value) =>
        value is not null ? Success(value) : Failure<T>(Error.NullValue);
}

O construtor protege duas regras importantes: um resultado bem-sucedido não pode conter um erro, e um resultado com falha deve conter um. Manter esses estados válidos evita objetos contraditórios, como um resultado bem-sucedido com uma mensagem de erro.

Para operações que retornam dados, a versão genérica armazena o valor obtido com sucesso:

public class Result<T> : Result
{
    private readonly T? _value;

    protected internal Result(T? value, bool isSuccess, Error error) : base(isSuccess, error)
        => _value = value;

    [NotNull]
    public T Value => _value! ?? throw new InvalidOperationException("Result has no value");

    public static implicit operator Result<T>(T? value) => Create(value);
}

A propriedade Value está disponível para resultados bem-sucedidos. Acessá-la após uma falha lança uma InvalidOperationException, portanto os consumidores devem verificar primeiro o estado do resultado. O padrão torna o contrato mais claro, mas ainda depende do uso correto no local da chamada.

Retornando um usuário sem retornar null

Agora, um método de busca pode comunicar explicitamente diferentes falhas esperadas:

public Result<User> GetUserById(int userId)
{
    if (userId <= 0)
        return Result.Failure<User>(Error.InvalidUserId);

    var user = _userRepository.FindById(userId);
    if (user == null)
        return Result.Failure<User>(Error.UserNotFound);

    return Result.Success(user);
}

Supondo que InvalidUserId e UserNotFound estejam definidos no catálogo de erros, o tipo de retorno informa aos consumidores que o método tem dois estados possíveis. O chamador pode tratá-los diretamente:

var result = GetUserById(123);

if (result.IsSuccess)
{
    Console.WriteLine($"User found: {result.Value.Name}");
}
else
{
    Console.WriteLine($"Failed to retrieve user: {result.Error.Message}");
}

Isso é mais descritivo do que verificar se um User retornado é null. Também mantém os resultados esperados de negócio visíveis no fluxo de controle normal.

Result não substitui exceções

As exceções continuam sendo apropriadas para condições inesperadas que não podem ser tratadas como parte da operação normal. As diretrizes do .NET recomendam evitar exceções para condições rotineiras e usar o tratamento de exceções para eventos verdadeiramente excepcionais. (learn.microsoft.com)

Uma distinção prática é:

  • Retorne um resultado para falhas de validação, registros ausentes, conflitos e outros resultados esperados.
  • Lance exceções para erros de programação, estado inválido do objeto ou falhas inesperadas de infraestrutura que a camada atual não consegue tratar de forma significativa.

O Result Pattern não tem como objetivo principal eliminar null ou exceções de todos os lugares. Seu objetivo é dar um tipo explícito às falhas esperadas e facilitar a compreensão dos contratos dos métodos.

Dois outros tratamentos práticos do padrão estão disponíveis em https://www.red-gate.com/simple-talk/development/dotnet-development/the-result-pattern-in-asp-net-core-minimal-apis/ e https://medium.com/@emrecantopaloglu/the-result-pattern-in-net-a-simple-guide-73a8f1b89d73.

Referências