📜 Além do Básico: Como Usar LanguageExt e Specification Pattern para Validar em .NET 📦

Neste artigo, vamos explorar uma abordagem para validação em .NET utilizando a biblioteca LanguageExt e o Specification Pattern para melhorar a legibilidade e a manutenção do código. No mundo .NET, a validação é feita de várias maneiras, mas muitas práticas comuns trazem limitações que impactam a escalabilidade e o desempenho do código. Aqui, vamos analisar essas práticas e construir uma solução mais limpa e funcional.

Problemas com as Abordagens Comuns de Validação

Frequentemente, vemos código de validação estruturado da seguinte forma:

comparação das abordagens de validação em .net

1. Abordagem Simples com Retorno de Booleano

Muitas vezes, métodos de validação retornam apenas true ou false, indicando se o objeto é válido. Isso, no entanto, limita o controle sobre os erros específicos e dificulta o manuseio detalhado de respostas de validação.

2. Abordagem de Lista de Erros (IEnumerable de Strings)

Outra prática comum é acumular erros em uma lista de strings, retornando essa lista ao final da validação. Isso melhora a visibilidade dos erros, mas pode ser confuso, pois requer que o consumidor do código interprete o significado de uma lista vazia como uma validação bem-sucedida. Além disso, essa abordagem carece de clareza em relação ao tipo e à natureza dos erros.

Ambas as abordagens sofrem com problemas de escalabilidade e complexidade, especialmente em cenários onde validações específicas podem ser aplicadas dinamicamente.

Uma Solução Funcional com LanguageExt e o Specification Pattern

A biblioteca LanguageExt oferece um conjunto poderoso de ferramentas funcionais em .NET. Utilizando monads, como o Either e o Validation, podemos estruturar validações de maneira mais clara e modular. Combinando o Specification Pattern, que permite a criação de regras reutilizáveis, conseguimos alcançar uma solução limpa e extensível.

1. Estruturando Validações com o Specification Pattern

O Specification Pattern é ideal para situações onde as regras de validação variam e podem ser combinadas. Vamos definir uma interface ISpecification com um método IsSatisfiedBy, que recebe um objeto e retorna true ou false, e um ErrorMessage, que armazena a mensagem de erro:

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T entity);
    string ErrorMessage { get; }
}

Em seguida, podemos implementar especificações como EmailSpecification, AgeSpecification, entre outras:

public class EmailSpecification : ISpecification<User>
{
    public bool IsSatisfiedBy(User user) =>
        !string.IsNullOrEmpty(user.Email) && user.Email.Contains("@");

    public string ErrorMessage => "Email inválido.";
}

public class AgeSpecification : ISpecification<User>
{
    public bool IsSatisfiedBy(User user) =>
        user.Age >= 18 && user.Age <= 120;

    public string ErrorMessage => "A idade deve estar entre 18 e 120.";
}

2. Combinando Especificações e Usando LanguageExt para Retorno de Erros

Com as especificações em vigor, podemos criar um método de validação que utiliza Validation<Error, T>, onde Error representa a coleção de erros acumulados e T é o tipo a ser validado. Em LanguageExt, o tipo Validation permite que múltiplas validações falhem simultaneamente, acumulando erros.

Primeiro, vamos definir o tipo ValidationError para encapsular as mensagens de erro:

public record ValidationError(string Message);

Em seguida, criamos um método que executa cada especificação e acumula erros usando Validation<ValidationError, User>:

public static Validation<ValidationError, User> ValidateUser(User user)
{
    var specifications = new List<ISpecification<User>>
    {
        new EmailSpecification(),
        new AgeSpecification()
    };

    var errors = specifications
        .Where(spec => !spec.IsSatisfiedBy(user))
        .Select(spec => new ValidationError(spec.ErrorMessage))
        .ToSeq();

    return errors.Any() ? Validation<ValidationError, User>.Fail(errors) : Validation<ValidationError, User>.Success(user);
}

Neste exemplo, se algum erro for encontrado, a validação retorna uma lista de ValidationError. Caso contrário, retorna o próprio objeto User, indicando que ele passou nas verificações.

3. Integrando com Respostas de API

Para um cenário de API, o uso de Validation permite mapear automaticamente o resultado em um IResult do .NET, facilitando o retorno de uma resposta apropriada:

public static IResult ValidateAndRespond(User user)
{
    var validationResult = ValidateUser(user);

    return validationResult.Match(
        success => Results.Ok(success),
        errors => Results.BadRequest(errors.Select(e => e.Message))
    );
}

Aqui, a função Match verifica se a validação foi bem-sucedida ou não. Em caso de sucesso, retornamos 200 OK com o objeto User; caso contrário, retornamos 400 Bad Request com as mensagens de erro.

4. Benefícios da Abordagem Funcional com LanguageExt

  • Clareza e Leitura: A estrutura funcional do código e o uso de Validation tornam o fluxo de validação intuitivo, sem necessidade de listas mutáveis.
  • Escalabilidade: O Specification Pattern permite adicionar e combinar regras de validação facilmente.
  • Desempenho: O código evita validações desnecessárias por meio de short-circuiting e não precisa alocar listas adicionais.
  • Facilidade de Integração: O resultado de Validation pode ser facilmente mapeado para respostas HTTP em APIs.

Benefícios da Abordagem Funcional

Conclusão

Essa combinação de LanguageExt com o Specification Pattern traz uma abordagem de validação modular e funcional, que facilita o manuseio de erros e melhora a escalabilidade. Além de organizar o código, esse método permite controlar a execução das regras de validação de maneira robusta e adaptável, favorecendo o desenvolvimento de aplicações mais seguras e de fácil manutenção em .NET.

Essa técnica pode ser estendida para diversos cenários onde validações complexas e múltiplas condições precisam ser gerenciadas. Para quem deseja explorar o potencial do LanguageExt e das técnicas funcionais em C#, essa é uma solução excelente para começar.