Esta documentação apresenta os conceitos, funcionalidades e exemplos práticos para usar a biblioteca SmartValidations em projetos .NET.
Para instruções objetivas de uso por ferramentas de IA, consulte também .docs/validations.ai-rules.md.
Projetos alvo: .NET 8, .NET 9 e .NET 10.
Sumário
- Introdução
- Funcionalidades Principais
- Exemplos de uso base
- Exemplos de uso com structs (IValidable)
- Exemplos de uso aninhados (objetos e coleções)
- Exemplos de uso avançados (condicionais, regras personalizadas, internacionalização)
- Referência da API
- Boas práticas
- Resumo
- Documentação para IA
SmartValidations é uma biblioteca de validação fluente e orientada a modelos para .NET. Ela produz Problems estruturados (em vez de exceções) ao aplicar regras em objetos, valores e coleções. É construída sobre SmartProblems, permitindo respostas padronizadas e serializáveis em APIs e UIs.
Benefícios principais:
- Validação fluente em uma única passagem com
RuleSet. - Sem exceções no fluxo esperado: retorna
Problemspadronizados. - Composição forte e tipada: uso de
INumber<T>,CallerArgumentExpressione genéricos. - Validação aninhada de primeira classe: objetos e coleções, com caminho de propriedade encadeado automaticamente.
- Preparada para APIs: mensagens localizáveis e metadados ricos para diagnóstico.
-
RuleSet- DSL fluente para aplicar regras. Cada falha gera um
ProblemviaProblems.InvalidParameter(...)com metadados:rule(nome da regra),current(valor atual),expected(valor(es) esperado(s)),pattern(em regras de regex),propertiesevalues(em regras com 2 operandos).
- Integra com display names e prefixos de propriedades, removendo prefixos configurados ao encadear problemas.
- Conversão implícita para
Problems?e métodoHasProblems(out Problems?). - É um
readonly ref struct: use localmente em métodos síncronos de validação, especialmente emHasProblems(out Problems?).
- DSL fluente para aplicar regras. Cada falha gera um
-
IValidable- Contrato simples com
HasProblems(out Problems?)para permitir validação de objetos e value objects (structs).
- Contrato simples com
-
Predicados públicos de apoio (
BuildInPredicates)- Conjunto abrangente de verificações: vazios, igualdade, comparações, faixas, tamanhos, padrões de string, e utilitários de data/tempo.
- Utilizados por
RuleSetpara implementar as regras fluentes.
-
Validações aninhadas
Nested(...)eNotNullNested(...)para objetos, coleções e tipos que implementamIValidable.- Encadeia
PropertycomChainPropertye índices (ex.:Items[2].Quantity).
-
Regras condicionais e customizadas
When(...)eUnless(...)(várias sobrecargas) para aplicar grupos de regras sob condição ou de forma alternativa.Must(...)eBothMust(...)para regras personalizadas com formatadores de mensagem.
-
Utilidades
WithPropertyPrefix(...)para normalizar caminhos removendo prefixos conhecidos.- Regras de e-mail, URL, URL HTTPS, URL absoluta e URL relativa.
Validação simples em um DTO usando RuleSet e retornando Problems via HasProblems:
using RoyalCode.SmartValidations;
using RoyalCode.SmartProblems;
public sealed class CreateUserRequest
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public bool HasProblems(out Problems? problems)
{
return Rules.Set<CreateUserRequest>()
.NotEmpty(Name)
.Min(Age, 18)
.HasProblems(out problems);
}
}Usando como serviço/handler, retornando Problems? diretamente:
public static Problems? ValidateProduct(string sku, string? name, decimal price)
{
return Rules.Set()
.NotEmpty(sku)
.NullOrLength(name, 3, 120)
.Min(price, 0m)
; // conversão implícita para Problems?
}Checando e serializando em API:
var set = Rules.Set().NotEmpty(request.Email).Email(request.Email);
if (set.HasProblems(out var problems))
{
// converter para ProblemDetails usando SmartProblems e retornar 400/422 conforme categoria
}Implemente IValidable em value objects e use RuleSet.Validate para coleções:
public readonly struct Money : IValidable
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public bool HasProblems(out Problems? problems)
{
return Rules.Set<Money>()
.Min(Amount, 0m)
.Length(Currency, 3, 3)
.HasProblems(out problems);
}
}
var prices = new[] { new Money(-1, ""), new Money(10, "USD") };
var set = Rules.Set().Validate(prices);
if (set.HasProblems(out var problems))
{
// apenas o item inválido gera problemas; o Property de cada problema
// será o nome do argumento com índice: "prices[0]"
}Observação: Validate substitui o Property dos problemas internos pelo nome do argumento (com índice em coleções). No exemplo, tanto a falha de Amount quanto a de Currency do primeiro item terão Property == "prices[0]"; o campo específico permanece no texto da mensagem. Esse comportamento é pensado para value objects, onde o nome externo é mais significativo que o campo interno — para preservar o caminho completo (ex.: Items[0].Quantity), use Nested com classes.
Validando objetos obrigatórios com NotNullNested e objetos opcionais com Nested:
public sealed class Address
{
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string ZipCode { get; set; } = string.Empty;
}
public sealed class CheckoutRequest : IValidable
{
public string CustomerId { get; set; } = string.Empty;
public Address? Shipping { get; set; }
public List<Address>? PastAddresses { get; set; }
public bool HasProblems(out Problems? problems)
{
return Rules.Set<CheckoutRequest>()
.NotEmpty(CustomerId)
.NotNullNested(Shipping, addr => Rules.Set<Address>()
.WithPropertyPrefix(nameof(addr))
.NotEmpty(addr.Street)
.NotEmpty(addr.City)
.NotEmpty(addr.ZipCode))
.Nested(PastAddresses, addr => Rules.Set<Address>()
.WithPropertyPrefix(nameof(addr))
.NotEmpty(addr.Street)
.NotEmpty(addr.City)
.NotEmpty(addr.ZipCode))
.HasProblems(out problems);
}
}Usando WithPropertyPrefix para normalizar nomes ao compor validadores reutilizáveis:
Problems? ValidateAddress(Address address)
=> Rules.Set<Address>()
.WithPropertyPrefix(nameof(address))
.NotEmpty(address.Street)
.NotEmpty(address.City)
.NotEmpty(address.ZipCode);
var order = new Order { ShippingAddress = new Address() };
var set2 = Rules.Set<Order>()
.WithPropertyPrefix(nameof(order))
.Nested(order.ShippingAddress, address => ValidateAddress(address));No exemplo acima, se Street estiver vazio, o Property do Problem será ShippingAddress.Street.
Sem o WithPropertyPrefix externo, o caminho manteria o nome da variável (order.ShippingAddress.Street). Sem o WithPropertyPrefix interno, o caminho incluiria o nome do parâmetro do validador (ShippingAddress.address.Street).
- Regras condicionais (
When/Unless):
var set = Rules.Set()
.When(isGuest, s => s.NotEmpty(email).Email(email))
.Unless(hasAddressOnFile, s => s
.NotEmpty(addr.Street)
.NotEmpty(addr.City)
.NotEmpty(addr.ZipCode));
// Grupos alternativos: adiciona problemas de ambos se ambos falharem
set = set.Unless(
s => s.NotEmpty(promoCode), // condição
s => s.Min(totalAmount, 100m)); // alternativo- URLs especializadas, sinais numéricos e datas:
var set = Rules.Set()
.HttpsUrl(callbackUrl)
.RelativeUrl(returnPath)
.Positive(quantity)
.Min(price, 0m)
.InFuture(expiresAt)
.After(periodEnd, periodStart);- Regras personalizadas (
Must/BothMust) com metadados de regra:
var strong = Rules.Set()
.Must(password,
p => p is { Length: >= 8 } && p.Any(char.IsDigit) && p.Any(char.IsUpper),
(prop, _) => $"{prop} must contain at least 8 chars, an uppercase and a digit.",
ruleName: "password.policy")
.BothMust(start, end,
(s, e) => s < e,
(p1, p2, _, _) => $"{p1} must be before {p2}.",
ruleName: "period.order");- Internacionalização
- As mensagens são formatadas por templates (ex.:
R.MinMessageTemplate) e nomes de exibição viaDisplayNames. - Os templates são recursos localizáveis (
R.resx); a biblioteca inclui inglês (padrão) ept-BR, selecionados pelaCultureInfo.CurrentUICultureda thread. - Configure seus display names (DataAnnotations ou provedor customizado) para mensagens amigáveis.
- As mensagens são formatadas por templates (ex.:
Tipos principais:
RuleSet(fluent API de validação)IValidable(contrato para validação)BuildInPredicates(predicados públicos de apoio usados pelas regras)
Escopo de uso do RuleSet
RuleSetéreadonly ref struct.- Use em escopo local e síncrono; não armazene em campos, não capture em lambdas assíncronas e não tente atravessar
await. - O uso principal esperado é dentro de
HasProblems(out Problems?)ou funções síncronas que retornamProblems?. - Use como uma única cadeia fluente: após a primeira falha, as cópias de um
RuleSetcompartilham a mesma coleção deProblems— não ramifique umRuleSetintermediário em cadeias independentes.
Criação e inspeção
Rules.Set()/Rules.Set<T>()/RuleSet.For<T>()HasProblems(out Problems? problems)- Conversão implícita
RuleSet -> Problems? WithPropertyPrefix(string)
Nulos e vazios
NotNull(value)NotEmptypara:string,INumber<T>,T? where T: struct, INumber<T>, arrays,ICollection<T>,IReadOnlyCollection<T>,IEnumerable<T>,DateTime(Offset),DateOnly,GuidNullOrNotEmptypara:string,INumber<T>,T? where T: struct, INumber<T>- Duais:
BothNullOrNotEmpty(string?, string?) - Semântica de "vazio": zero para números,
MinValuepara datas,Guid.Emptypara GUIDs, nula/em branco para strings, sem itens para coleções.
Igualdade/Desigualdade
EqualeNotEqualparastring(comStringComparison) e tiposIEquatable<T>(inclui versõesNullable<T>)- Duais:
BothEqualeBothNotEqualparastringe tiposIEquatable<T>
Strings e padrões
Matches/NotMatchescomstring patternouRegexStartsWith/EndsWith/Contains/NotContainOnlyLetters/OnlyDigits/OnlyLettersOrDigits/NoWhiteSpace- As sobrecargas com
string patternaplicamBuildInPredicates.RegexMatchTimeout(1s) como proteção contra backtracking catastrófico.
Numéricos e faixas
Min/Max/MinMax(e variantesNullOrMin,NullOrMax,NullOrMinMax)Positive/Negative/Zero/NotZero- Tamanho de string:
MinLength/MaxLength/Length(eNullOrMinLength,NullOrMaxLength,NullOrLength)
Comparações relativas
LessThan/LessThanOrEqual/GreaterThan/GreaterThanOrEqual(para tiposIComparable<T>e suas variantesNullable)- Nas variantes
Nullable,nullé tratado como o menor valor possível (mesma convenção deComparer<T>.Default).
Datas e horários
InPast/InFuture/TodayparaDateTime,DateTimeOffset,DateOnlyAfter/Before/Betweenpara os mesmos tipos- As regras relativas (
InPast,InFuture,Today) usamBuildInPredicates.Clock(TimeProvider, padrãoTimeProvider.System); substitua em testes para resultados determinísticos.
E-mail e URL
Email(string?)eUrl(string?)HttpsUrl(string?),AbsoluteUrl(string?)eRelativeUrl(string?)
Customização
Must<T>(value, predicate, messageFormatter[, ruleName])Must<TValue, TParam>(value, param, predicate, messageFormatter[, ruleName])BothMust<T1, T2>(...)eBothMust<T1, T2, TParam>(...)
Validação aninhada
- Objetos opcionais:
Nested(value, Func<T, Problems?>)/Nested(value, Func<T, ValidateFunc>)/Nested(value) where T: IValidable - Coleções opcionais:
Nested(IEnumerable<T>, ...)com indexação automática; itensnullsão ignorados - Objetos e coleções obrigatórios:
NotNullNested(...)com as mesmas variações; gera problema quando o valor/coleção énulle, em coleções, quando um item énull(com propriedade indexada, ex.:Items[2]).
Structs com IValidable
Validate<T>(value) where T: struct, IValidableValidate<T>(IEnumerable<T>) where T: struct, IValidable
Condicionais
When(bool, RuleSetBuilder)Unless(...)com múltiplas sobrecargas: combinando builders, fábricas e resultados previamente avaliados
Metadados em Problem (SmartProblems)
rule,current,expected,pattern,properties,valuespreenchidos conforme a regra.
- Modele validação por request/DTO em uma única função que retorna
Problems?. - Use
IValidable/ValidateFuncpara compor validações de agregados, objetos aninhados e value objects. - Prefira regras não-explosivas: use
Result/Problemsem vez de exceções em controle de fluxo. - Defina
StringComparisonexplicitamente quando relevante. - Use
WithPropertyPrefixpara normalizar caminhos ao reutilizar validadores. - Padronize mensagens com templates localizáveis e display names consistentes.
- Garanta cobertura com regras
NullOr*quando campos forem opcionais. - Para objetos aninhados opcionais, use
Nested; para obrigatórios, useNotNullNested. - Para limites fixos, prefira
Min,Max,MinMax,Positive,Negative,ZeroeNotZero; deixeLessThan/GreaterThanpara comparação entre valores. - Use
HttpsUrl,AbsoluteUrl,RelativeUrle regras de data diretamente noRuleSetantes de recorrer aMust. - Não armazene
RuleSetfora do escopo local de validação; ele é umref struct. - Trate o
RuleSetcomo uma cadeia fluente única; não ramifique um set intermediário em cadeias independentes (as cópias compartilham osProblems). - Em coleções, valide cada item com
Nested/Validatepara obter caminhos com índice.
SmartValidations fornece uma maneira fluente, fortemente tipada e performática de validar modelos .NET. Ao invés de lançar exceções, as falhas são representadas por Problems ricos em contexto, integráveis com ProblemDetails em APIs. Suas APIs cobrem desde regras básicas de vazio/igualdade até validações aninhadas, condicionais e customizadas, com metadados para rastreabilidade e mensagens prontas para localização.
Use .docs/validations.ai-rules.md como documento de instruções para ferramentas de IA em outros projetos e repositórios.