Open source

PMQ.Notifications

nível aplicado-em-producao

1.550 downloads · o mais adotado da suíte · repositório

Como uma regra de negócio reclama, num sistema .NET, acumulando o que está errado em vez de lançar exceção no primeiro problema.

O problema

“Valor da devolução maior que o disponível” é um resultado previsto: o programa antecipou que isso pode acontecer e tem opinião sobre. Tratar com exceção tem dois custos: ela interrompe no primeiro problema, quando a tela quer mostrar os cinco campos errados de uma vez; e apaga a diferença entre “o cliente errou o CPF” e “o banco caiu”, que merecem log, alerta e tratamento completamente diferentes.

A escolha está escrita no próprio código:

/// Accumulates rule violations instead of throwing, so that a broken business rule stays an
/// expected outcome rather than exceptional control flow. Callers check IsValid and decide
/// what to do with ValidationResult.
public abstract class Validatable
{
    [JsonIgnore] public ValidationResult ValidationResult { get; } = new();
    [JsonIgnore] public bool IsValid => ValidationResult.IsValid;
    [JsonIgnore] public bool IsInvalid => !IsValid;
}

Três decisões que eu defendo aqui

Categoria é objeto de valor, não enum. Um enum fecha o conjunto: quem consome o pacote não consegue acrescentar uma categoria sem alterar a biblioteca. Aqui NotificationType é uma classe selada com instâncias estáticas: Validation, BusinessRule, NotFound, InconsistentState, AccessDenied, mais um Custom(valor) para o caso que eu não previ:

public sealed class NotificationType : IEquatable<NotificationType>
{
    public static readonly NotificationType BusinessRule = new("BUSINESSRULE");
    public static readonly NotificationType NotFound     = new("NOTFOUND");

    public static NotificationType Custom(string value) => new(value);
}

Custa mais código que um enum e paga na primeira vez que alguém precisa de uma categoria que a biblioteca não tem. Igualdade por valor, com == e GetHashCode, para o tipo se comportar como dado e não como referência.

[JsonIgnore] no estado de validação. Detalhe pequeno com consequência real: sem isso, o objeto serializado numa resposta de API leva junto a lista de erros internos. É o tipo de vazamento que ninguém percebe até aparecer no payload de produção.

Construído sobre o ValidationResult do FluentValidation, e não sobre um tipo próprio. Perde-se independência e ganha-se interoperabilidade com o validador que praticamente todo projeto .NET já usa, porque quem já escreve validador não aprende um segundo vocabulário.

A fraqueza que eu aceito

Nada obriga o chamador a verificar IsValid. Esquecer produz sucesso silencioso, que é o pior tipo de defeito. Um tipo Result teria o compilador como garantia, mas contamina toda assinatura no caminho e obrigaria a conviver com duas convenções no mesmo código, o que é pior que qualquer uma das duas.

Na prática a fraqueza acabou resolvida por outro lado: quando o PMQ.Mediator ganhou pipeline, a verificação passou a acontecer num estágio antes do handler, e o caso de uso nem executa se as notificações não estiverem limpas.

Por que é o mais baixado da suíte

Porque é o problema mais universal. Todo sistema com regra de negócio precisa decidir como a regra reclama, e a resposta idiomática do .NET, que é lançar exceção, é a errada para a maioria dos casos.

Pablo Mickael Quevedo Senior Software Engineer · Novo Hamburgo, RS