Flunt

O Flunt é uma biblioteca open source para .NET que implementa o padrão de notificações, concentrando erros e mudanças das suas entidades e ações em um único lugar. Em vez de lançar uma exceção a cada regra quebrada, você acumula notificações e decide o que fazer com elas no momento certo.

Neste artigo, você vai entender de onde vem a ideia, como instalar o pacote e como trabalhar com notificações, notificações customizadas, contratos e mensagens localizadas. Todo o conteúdo considera a versão 2.x do pacote, que trouxe mudanças importantes em relação à linha 1.x.

O que é o Flunt

O Flunt nasceu de duas necessidades bem claras. A primeira é implementar o Domain Notification Pattern para substituir exceptions no nível de domínio da aplicação.

A segunda é reduzir a quantidade de IFs, ou seja, a complexidade ciclomática, usando uma abordagem por contratos. Assim, as regras ficam declarativas e fáceis de ler.

Na prática, o que o Flunt faz é adicionar uma lista de Notification à sua classe, junto com diversos métodos para interagir com ela. Além disso, o pacote traz um conjunto grande de validações prontas e testadas.

O projeto é mantido por André Baltieri, tem licença MIT e o código fonte está disponível no repositório oficial no GitHub. Por isso, você pode estudar a implementação, abrir issues e contribuir.

Por que trocar exceptions por notificações

Uma regra de validação que falha não é um evento excepcional. Pelo contrário, ela faz parte do fluxo normal de uma aplicação que recebe dados de usuários.

Martin Fowler defende exatamente isso no artigo Replacing Throwing Exceptions with Notification in Validations. Segundo ele, um objeto de notificação coleta todos os erros, em vez de interromper a execução no primeiro problema.

Com isso, o usuário recebe a lista completa do que precisa corrigir em uma única resposta. Ele não precisa descobrir os erros um a um, a cada nova tentativa.

A documentação de arquitetura de microsserviços da Microsoft segue a mesma linha. O guia Design validations in the domain model layer cita o uso combinado dos padrões Specification e Notification para aplicar regras de validação no domínio.

Se quiser se aprofundar nessa comparação, leia também o artigo sobre exceptions vs domain notification aqui do balta.io. Além disso, o texto sobre exceptions vs Result Pattern no .NET mostra outra alternativa para o mesmo problema.

Breaking changes na versão 2.x

Se você usa o Flunt em versões inferiores à 2.0.0, recomendo ler a documentação e o código fonte antes de migrar. A versão 2.x inclui diversas mudanças que vão quebrar o seu código.

Por exemplo, a classe Notifiable passou a ser genérica (Notifiable<T>), e a checagem de validade agora usa a propriedade IsValid. Da mesma forma, os contratos passaram a herdar de Contract<T> e ganharam mensagens padrão.

O próprio pacote deixa isso explícito nas notas de versão, que avisam sobre breaking changes a partir da linha 1.5. Portanto, trate a atualização como uma migração planejada, e não como um simples update de dependência.

Como instalar o Flunt

Para começar a trabalhar com o Flunt, basta instalar o pacote pelo Visual Studio ou pelo terminal, com o comando abaixo.

dotnet add package Flunt --version 2.0.0

Se preferir o console do gerenciador de pacotes do Visual Studio, o comando equivalente também funciona.

Install-Package Flunt

O pacote se baseia no .NET Standard 2.0. Então, se o seu projeto tem como target qualquer versão do .NET compatível com essa especificação, você não terá problemas para instalá-lo.

Isso inclui o .NET Core, as versões modernas do .NET e o .NET Framework a partir da 4.6.1. Você pode conferir a tabela completa na documentação do .NET Standard.

Notificações no Flunt

O coração da biblioteca é a classe Notification. Ela é a notificação base, a mais simples possível, com apenas uma chave e uma mensagem.

public class Notification
{
    public Notification()
    {

    }

    public Notification(string key, string message)
    {
        Key = key;
        Message = message;
    }

    public string Key { get; set; }
    public string Message { get; set; }
}

A chave normalmente identifica a propriedade ou o campo que gerou o problema. Já a mensagem descreve o que está errado, em um texto que pode ir direto para o usuário.

O pacote faz uso dessa notificação por meio da classe abstrata Notifiable<T>, que guarda a lista de notificações e expõe os métodos de interação.

public abstract class Notifiable<T> where T : Notification
{
    // Lista interna, exposta como IReadOnlyCollection<T>
    ...
}

Herdando de Notifiable

Para começar a concentrar notificações em uma classe, tudo o que você precisa fazer é herdar de Notifiable<T>. Nesse caso, T é um tipo de Notification, como no exemplo a seguir.

public class Customer : Notifiable<Notification>
{
    ...
}

Feito isso, você ganha várias possibilidades para interagir com as notificações da classe. Por exemplo, o método AddNotification() e a propriedade IsValid.

var customer = new Customer();
customer.AddNotification("Key", "Value");
var isValid = customer.IsValid; // Retorna false, pois adicionamos uma notificação

foreach (var item in customer.Notifications)
{
    // Percorre todas as notificações
}

A propriedade IsValid retorna true somente quando a lista está vazia. Ou seja, basta uma notificação para o objeto ser considerado inválido.

Métodos disponíveis no Notifiable

Ao olhar o código fonte do Flunt, você encontra um conjunto pequeno e objetivo de membros. Primeiro, a propriedade Notifications expõe a lista como uma coleção somente leitura.

Em seguida, o método AddNotification tem sobrecargas que recebem uma chave e uma mensagem, uma instância de T ou um Type cujo nome vira a chave. Dessa forma, você escolhe a forma mais conveniente para cada situação.

O método AddNotifications também tem várias sobrecargas. Ele aceita coleções de notificações e também outros objetos Notifiable<T>, inclusive vários de uma vez.

Por fim, o método Clear() limpa a lista. Isso é útil quando o mesmo objeto passa por uma nova rodada de validação e você não quer manter os erros antigos.

Notificações customizadas no Flunt

Caso queira ter mais informações nas notificações, você pode criar uma notificação customizada. Ela herda de Notification e tem propriedades adicionais, como uma data.

public class CustomNotification : Notification
{
    public CustomNotification(string key, string message)
    {
        Key = key;
        Message = message;
        Date = DateTime.Now;
    }

    public CustomNotification(string key, string message, DateTime date)
    {
        Key = key;
        Message = message;
        Date = date;
    }

    // Nova propriedade
    public DateTime Date { get; set; }
}

Por padrão, o Flunt não sabe como interagir com a sua notificação customizada. Então, você precisa explicar para ele como converter uma Notification em uma CustomNotification, e isso acontece por meio de um CustomNotifiable.

public class CustomNotifiable : Notifiable<CustomNotification>, INotifiable
{
    public void AddNotifications(IEnumerable<Notification> notifications)
    {
        foreach (var item in notifications)
        {
            var notification = new CustomNotification(item.Key, item.Message, DateTime.Now);
            AddNotification(notification);
        }
    }
}

O seu CustomNotifiable precisa implementar a interface INotifiable, que contém o método responsável por converter as notificações em CustomNotification. Assim, as notificações geradas pelos contratos chegam à sua classe já no formato customizado.

public class Customer : CustomNotifiable
{
    public string Name { get; set; }
}

var customer = new Customer();
customer.AddNotification(new CustomNotification("Key", "Value"));
var isValid = customer.IsValid; // Retorna false, pois adicionamos uma notificação

foreach (var item in customer.Notifications)
{
    // Cada item agora é um CustomNotification, com a propriedade Date
}

Note que agora o Customer herda de CustomNotifiable, e não de Notifiable<Notification>. Isso define que você está usando as notificações customizadas, e não as padrões do pacote.

Esse mesmo formato aparece nos testes de unidade do repositório oficial. Por isso, vale consultar o arquivo de testes de notificações customizadas quando tiver dúvidas sobre o comportamento esperado.

Validações prontas do Flunt

Só a implementação das notificações não justificaria um pacote. Por isso, o Flunt também traz muitas validações, muitas mesmo, que você pode adicionar aos seus contratos.

A cada versão, o projeto ganha novas validações, e todas são cobertas por testes. Dessa forma, você não precisa duplicar esses testes na sua aplicação.

As validações estão organizadas por tipo no código fonte. Há arquivos para string, int, long, decimal, double, float, bool, DateTime, TimeSpan, Guid, listas, objetos, expressões regulares, e-mails, URLs e cartões de crédito.

Entre os métodos mais usados, estão IsNotNullOrEmpty, IsNotNullOrWhiteSpace, IsGreaterThan, IsLowerThan, IsBetween, AreEquals, Contains, Matches, IsEmail, IsUrl e IsCreditCard. Além disso, muitos têm a versão negativa, como IsNotEmail e IsNotUrl.

Para ver todas as validações disponíveis, visite a pasta Validations no GitHub. Lá você encontra cada contrato com a documentação XML de cada método.

Um detalhe importante está nas validações de string. Nelas, IsGreaterThan e IsLowerThan comparam o tamanho do texto, e não o seu valor.

Contratos e especificações

Para usar uma validação, você precisa de um contrato, que pode ser criado sobre qualquer classe. Normalmente, criamos contratos para entidades ou comandos do domínio, mas fique à vontade para dar asas à imaginação.

public class CreateEmailContract : Contract<Email>
{
    public CreateEmailContract(Email email)
    {
        Requires()
            .IsEmail(email.Address, "Email");
    }
}

No exemplo acima, temos um contrato que valida a criação de um e-mail. Ele herda de Contract<Email> e, com isso, pode usar o método Requires() seguido da validação IsEmail que vem no pacote.

Como cada validação retorna o próprio contrato, você encadeia quantas regras quiser em uma interface fluente. Inclusive, é daí que vem o nome do projeto, uma junção de fluent com notifications.

O mesmo código sem o Flunt

Para comparar, veja como ficaria a mesma regra sem os contratos e sem as validações do Flunt. Nesse caso, você escreve a expressão regular e o IF manualmente.

public class EmailValidator
{
    public List<string> Errors { get; } = new();

    public EmailValidator(Email email)
    {
        if (!Regex.IsMatch(email.Address ?? "", @"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$"))
            Errors.Add("Invalid email");
    }
}

Imagine agora esse padrão repetido para nome, documento, data de nascimento e valores. Em pouco tempo, o construtor vira uma sequência longa de IFs que precisam de testes próprios.

Concentrando notificações nas entidades

Sempre que você chamar o construtor do CreateEmailContract passando um e-mail, a validação será executada. Além disso, você pode concentrar as notificações em outras entidades, passando o contrato para elas.

public class Email : Notifiable<Notification>
{
    public Email(string address)
    {
        Address = address;
        AddNotifications(new CreateEmailContract(this));
    }

    public string Address { get; private set; }
}

Isso funciona porque Contract<T> também herda de Notifiable<Notification>. Ou seja, um contrato é ele mesmo um objeto notificável, e suas notificações podem ser copiadas para a entidade.

O contrato ainda oferece o método Join, que agrega notificações de outros objetos notificáveis inválidos. Assim, você monta validações compostas sem repetir regras.

Usando o Flunt em um handler

O projeto de exemplos do repositório mostra um fluxo completo, com requisição, entidade, objeto de valor e handler. Primeiro, a requisição valida os dados de entrada com o próprio contrato.

public class CreateCustomerRequestContract : Contract<CreateCustomerRequest>
{
    public CreateCustomerRequestContract(CreateCustomerRequest request)
    {
        Requires()
            .IsNotNullOrEmpty(request.Name, "Name", "Custom error message")
            .IsEmail(request.Email, "Email");
    }
}

Em seguida, o handler também herda de Notifiable<Notification> e agrega as notificações de cada objeto envolvido na operação.

public class CustomerHandler : Notifiable<Notification>
{
    public bool Handle(CreateCustomerRequest request)
    {
        // Falha rápido se a requisição já for inválida
        if (request.IsValid == false)
        {
            AddNotifications(request.Notifications);
            return false;
        }

        var email = new Email(request.Email);
        var customer = new Customer(request.Name, email);

        AddNotifications(email.Notifications);
        AddNotifications(customer.Notifications);

        return IsValid;
    }
}

Essa estrutura aplica a ideia de fail fast validation descrita na wiki do projeto. Isto é, você barra dados inválidos antes mesmo de chegar às regras de negócio.

Depois disso, quem chama o handler percorre a lista Notifications para exibir ou devolver os erros. Em uma API, por exemplo, essa lista pode virar o corpo de uma resposta 400.

Se você trabalha com Minimal APIs, o artigo sobre validação em Minimal APIs no .NET 10 mostra outras formas de tratar a entrada antes do domínio. Ainda assim, as notificações do Flunt continuam úteis dentro das entidades.

O exemplo completo está disponível na pasta Flunt.Samples do repositório.

Localização das mensagens do Flunt

Outra novidade da versão 2.0 do Flunt são as mensagens padrão. Sempre que você chama uma validação como IsEmail, pode informar uma mensagem customizada ou usar a mensagem padrão, que nesse caso é should be an email.

As mensagens ficam na classe estática FluntErrorMessages, no namespace Flunt.Localization. Como elas são campos estáticos públicos, você pode sobrescrevê-las substituindo seus valores.

// Mensagem padrão traduzida para português
FluntErrorMessages.IsEmailLocalizationErrorMessage = "deve ser um E-mail";

A mensagem final combina a chave com o texto localizado. Portanto, uma falha em IsEmail com a chave Email gera algo como Email deve ser um E-mail depois da troca.

O ideal é fazer essa configuração uma única vez, na inicialização da aplicação. Afinal, os valores são globais e valem para todos os contratos.

Extensões e ports do Flunt

O README do projeto lista duas extensões oficiais. A Flunt.Extensions.Br traz validações voltadas ao Brasil, enquanto a Flunt.Extensions.AspNet integra o pacote ao ASP.NET.

Além disso, a comunidade criou versões da biblioteca para outras linguagens. Há ports para Java, JavaScript, Python e PHP, todos inspirados no projeto original em .NET.

Isso mostra que a ideia de contratos fluentes com notificações não depende do C#. Na verdade, ela se adapta bem a qualquer linguagem orientada a objetos.

Problemas, bugs e contribuições

Caso encontre um bug ou qualquer outro problema, fique à vontade para abrir uma issue no GitHub do projeto.

Agradeço de coração a todas as pessoas que contribuem com o Flunt, porque o pacote sozinho não seria nada. Você pode conferir a lista de contribuidores aqui.

Caso queira contribuir com o pacote, basta seguir estes passos:

  • Não faça Pull Request para a branch main
  • Siga a estrutura do projeto
  • Escreva pelo menos um teste de unidade

O último ponto é essencial, pois a confiabilidade da biblioteca vem justamente da cobertura de testes de cada validação.

Conclusão

O Flunt oferece uma forma simples de aplicar o padrão de notificações no .NET. Com ele, as entidades acumulam erros em vez de lançar exceptions, e o código fica mais legível graças aos contratos fluentes.

Além disso, as validações prontas e testadas reduzem a quantidade de IFs e de testes repetidos na sua aplicação. Por fim, as mensagens localizáveis e as notificações customizadas dão flexibilidade para adaptar o pacote ao seu domínio.

Se você está começando, instale o Flunt em um projeto de teste e crie um contrato para uma entidade simples. Em seguida, evolua para handlers que agregam notificações de várias entidades.

FAQ

O Flunt funciona com as versões atuais do .NET?

Sim. O pacote tem como target o .NET Standard 2.0, que é compatível com o .NET Core, o .NET moderno e o .NET Framework 4.6.1 ou superior.

Qual a diferença entre IsValid e a versão 1.x?

Na linha 1.x, a validade era checada pelas propriedades Valid e Invalid. Na versão 2.x, a checagem passou para a propriedade IsValid da classe genérica Notifiable<T>.

Posso usar mensagens em português nas validações?

Sim. Você pode passar uma mensagem customizada em cada validação ou sobrescrever as mensagens padrão na classe FluntErrorMessages.

O Flunt substitui todas as exceptions da aplicação?

Não. Ele substitui exceptions de validação de domínio, mas falhas realmente excepcionais, como indisponibilidade de banco de dados, continuam sendo exceptions.

Como juntar notificações de várias entidades?

Use o método AddNotifications passando os objetos notificáveis ou suas listas. Nos contratos, o método Join também agrega notificações de outros objetos.