Esta documentação apresenta os conceitos, funcionalidades e exemplos práticos para usar a biblioteca SmartProblems em projetos .NET. Serve também como referência para ferramentas de IA (ex.: GitHub Copilot) compreenderem e gerarem código de forma correta com base na API da biblioteca.
Projetos alvo: .NET 8, .NET 9 e .NET 10.
Para IA/agentes: se você precisa apenas gerar código correto, use
problems.ai-rules.md— imperativo, autocontido, com tabela de pacotes, cheat-sheet de assinaturas e anti-padrões. Este arquivo é o guia longo, com o "porquê".Verificado contra:
RoyalCode.SmartProblems1.0.0-preview-7.0 (net8.0 / net9.0 / net10.0). Ao alterar a API pública, atualize esta linha junto com o exemplo afetado. Se a versão instalada no seu projeto for outra, a documentação XML do pacote é a fonte da verdade — este arquivo é o guia de uso e de padrões.
Nota para IA e IDEs: este arquivo cobre quando e por que usar cada API, além das armadilhas que
o IntelliSense não revela (§8). Para confirmar sobrecargas, genéricos, retorno exato e ordem de parâmetros,
consulte a documentação XML das bibliotecas no pacote/IDE — especialmente em Result, Result<TValue>,
FindResult<TEntity>, FindResult<TEntity,TId>, FindCriteria<TEntity> e AsyncResultExtensions.
Antes de escrever a primeira linha de código, resolva pacote e using pela tabela da §1.1: os nomes de
pacote e de namespace divergem em vários casos e não são dedutíveis.
SmartProblems padroniza o tratamento de resultados e erros de operações em .NET, evitando exceções para fluxo normal e tornando o código previsível e composable.
Conceitos principais:
Problem: representa um erro com categoria, detalhe, propriedade e extensões.Problems: coleção deProblem(encadeável, iterável, conversível paraResult).Result/Result<T>: resultado de operação (sucesso/falha), com APIs de composição/transformação.FindResult<T>/FindResult<T, TId>: resultado de busca, com utilitários para continuar/mapear e converter paraResult.- Conversões para
ProblemDetails(RFC 9457) para uso em APIs. - Extensões para Entity Framework (métodos
TryFind*).
Resolva isto antes de gerar código. O nome do pacote NuGet e o nome do namespace divergem em
vários casos — as linhas marcadas com
| Tipo / membro | using (namespace) |
Pacote NuGet |
|---|---|---|
Problem, Problems, Result, Result<T> |
RoyalCode.SmartProblems |
RoyalCode.SmartProblems |
FindResult<>, Id<,>, FindCriterion, FindCriteria<>, FindCriteriaExtractor, DisplayNames |
RoyalCode.SmartProblems.Entities |
RoyalCode.SmartProblems |
TryFindAsync, TryFindByAsync, FindByCriteria, AddTo, SaveChanges, RemoveFromAsync |
Microsoft.EntityFrameworkCore |
RoyalCode.SmartProblems.EntityFramework |
OkMatch, OkMatch<T>, CreatedMatch<T>, AcceptedMatch, AcceptedMatch<T>, NoContentMatch (tipos) |
RoyalCode.SmartProblems.HttpResults |
RoyalCode.SmartProblems.ApiResults |
.OkMatch(), .CreatedMatch(), .AcceptedMatch(), .NoContentMatch() (extensions) |
Microsoft.AspNetCore.Http |
RoyalCode.SmartProblems.ApiResults |
WithExceptionFilter |
Microsoft.AspNetCore.Builder |
RoyalCode.SmartProblems.ApiResults |
ToActionResult e afins (MVC) |
Microsoft.AspNetCore.Mvc |
RoyalCode.SmartProblems.ApiResults |
ToResultAsync, FailureTypeReader |
System.Net.Http / RoyalCode.SmartProblems.Http |
RoyalCode.SmartProblems.Http |
ToProblemDetails(options), ProblemDetailsExtended |
RoyalCode.SmartProblems.Conversions |
RoyalCode.SmartProblems.ProblemDetails |
ProblemDetailsOptions, ProblemDetailsDescription |
RoyalCode.SmartProblems.Descriptions |
RoyalCode.SmartProblems.ProblemDetails |
AddProblemDetailsDescriptions |
Microsoft.Extensions.DependencyInjection |
RoyalCode.SmartProblems.ProblemDetails |
MapProblemDetailsDescriptionPage |
Microsoft.AspNetCore.Builder |
RoyalCode.SmartProblems.ProblemDetails |
EnsureIsValid, ToProblems, HasProblems (validator) |
FluentValidation |
RoyalCode.SmartProblems.FluentValidation |
Pontos que causam erro de compilação ou pacote faltante com mais frequência:
- Os tipos
OkMatche família estão no pacoteApiResults, mas no namespaceHttpResults. - Os métodos
.OkMatch(),.CreatedMatch(),.AcceptedMatch()e.NoContentMatch()são extensions emMicrosoft.AspNetCore.Http. ToResultAsyncé injetado emSystem.Net.Http(namespace já implícito comImplicitUsings), então "compila semusing" — mas exige o pacoteRoyalCode.SmartProblems.Http. Os tipos auxiliares (FailureTypeReader) ficam emRoyalCode.SmartProblems.Http.ToProblemDetailsestá no namespace...Conversions, porém é entregue pelo pacoteRoyalCode.SmartProblems.ProblemDetails(o pacote...Conversionstraz a serialização eProblemDetailsExtended, não a conversão a partir deProblems).- As extensões de EF e de ASP.NET Core usam os namespaces da Microsoft de propósito: instalado o pacote,
os métodos aparecem sem
usingnovo.
using canônicos por cenário:
// Serviço de domínio (sem EF, sem HTTP)
using RoyalCode.SmartProblems;
// Serviço com EF
using Microsoft.EntityFrameworkCore; // TryFindAsync, FindByCriteria, AddTo, SaveChanges
using RoyalCode.SmartProblems; // Result, Problems
using RoyalCode.SmartProblems.Entities; // FindResult, Id, FindCriterion
// Minimal API
using Microsoft.AspNetCore.Http; // OkMatch/CreatedMatch/NoContentMatch extension methods
using Microsoft.AspNetCore.Builder; // WithExceptionFilter, MapProblemDetailsDescriptionPage
using Microsoft.Extensions.DependencyInjection; // AddProblemDetailsDescriptions
using RoyalCode.SmartProblems;
using RoyalCode.SmartProblems.HttpResults; // OkMatch, CreatedMatch, NoContentMatch
using RoyalCode.SmartProblems.Descriptions; // ProblemDetailsOptions, ProblemDetailsDescription
// Cliente HTTP
using RoyalCode.SmartProblems;
using RoyalCode.SmartProblems.Http; // FailureTypeReader (ToResultAsync já vem de System.Net.Http)
// Validação com FluentValidation
using FluentValidation; // EnsureIsValid, ToProblems
using RoyalCode.SmartProblems;-
Modelagem de erros
- Categorias:
NotFound,InvalidParameter,ValidationFailed,NotAllowed,InvalidState,InternalServerError,CustomProblem. - Campos:
Detail,Property,TypeId,Extensions. - Utilitários:
With(key, value),ChainProperty(parent[, index]),ReplaceProperty(newProp).
- Categorias:
-
Coleção de erros (
Problems)- Operadores: implícito de
ProblemparaProblems,+para agregar problemas. - Iteração, indexador,
Contains,CopyTo,Count. - Conversão para
Resulte paraInvalidOperationException(ToException(...)).
- Operadores: implícito de
-
Resultados (
Result,Result<T>)- Construção implícita a partir de valor,
Problem,Problems,Exception. - Consultas:
IsSuccess,HasProblems(out problems),HasValue(out value). - Composição:
Match,Map,Continue,Collect, variantesAsync. - Soma de problemas entre resultados (
+=).
- Construção implícita a partir de valor,
-
Busca segura (
FindResult<T>,FindResult<T, TId>)- Avaliação:
Found,NotFound(out problem),HasInvalidParameter(...). - Composição:
Collect,Continue,Map(+Async). - Conversão:
ToResult([parameterName]). - Fábrica:
FindResult<TEntity>.Problem(byName, propertyName, propertyValue). - Fábrica multi-critério:
FindResult<TEntity>.Problem(ReadOnlySpan<FindCriterion>).
- Avaliação:
-
Conversão para
ProblemDetailsProblems.ToProblemDetails(options): namespaceRoyalCode.SmartProblems.Conversions, entregue pelo pacoteRoyalCode.SmartProblems.ProblemDetails(ver §1.1).ProblemDetailsExtendedagrega múltiplos problemas (errors,not_found,inner_details).- Personalização via
ProblemDetailsOptions,ProblemDetailsDescriptor,ProblemDetailsDescriptione arquivos JSON. - Página HTML de catálogo via
MapProblemDetailsDescriptionPage().
-
Integrações
- Entity Framework:
SmartProblemsEFExtensionscomTryFindAsync/TryFindByAsync,FindByCriteriaeFindResult. - FluentValidation:
ValidationsExtensions(ToProblems,HasProblems,EnsureIsValid,Validate/ValidateAsync). - HTTP/ASP.NET: utilitários para converter para
ProblemDetailse resultados de API.
- Entity Framework:
-
Tratamento de exceções
Problems.InternalError(Exception?, ExceptionOptions?)com controle de mensagem, tipo e stack trace.Problems.ExceptionHandlerpara mapear exceções customizadas emProblem.
Antes dos exemplos, é importante entender que os problemas são criados por categoria, cada uma mapeando para um HTTP Status Code e um cenário recomendado de uso. A seguir, as categorias suportadas, o status associado e quando usar:
-
InvalidParameter→ 400 Bad Request- Quando: entrada inválida do cliente (formato, range, campos obrigatórios, enum inválido). Ideal para validações de request e regras de entrada.
- Dica: use
Propertypara apontar o campo específico; agregue várias ocorrências em uma única resposta.
-
ValidationFailed→ 422 Unprocessable Entity- Quando: regras de domínio/negócio foram violadas embora a entrada seja sintaticamente válida (ex.: estado inconsistente, combinação inválida). Foca em validação semântica.
-
NotAllowed→ 403 Forbidden- Quando: operação proibida devido a autorização/política/regra (ex.: usuário sem permissão, janela de operação fechada).
-
InvalidState→ 409 Conflict- Quando: conflito de estado ou transição inválida (ex.: pedido já concluído, recurso bloqueado).
-
NotFound→ 404 Not Found- Quando: recurso não existe (ex.: ID inexistente, filtro não encontrou registro).
-
InternalServerError→ 500 Internal Server Error- Quando: erro inesperado no servidor (exceptions não tratadas, falha de infraestrutura). Não use para erros esperados de domínio.
-
CustomProblem→ definido pela descrição (ProblemDetails) do seu tipo- Quando: erro específico de domínio que não se encaixa nas categorias padrão; requer
typeIde descrição viaProblemDetailsOptions.
- Quando: erro específico de domínio que não se encaixa nas categorias padrão; requer
Separação de Custom e Exception:
- Custom: use
Problems.Custom(detail, typeId, property)para erros de domínio descritos pela sua API. - Exception: use
Problems.InternalError(exception)para exceptions inesperadas; configureExceptionOptionseExceptionHandlerse necessário.
Exemplos por categoria:
// 400 Bad Request – entrada inválida
var p400 = Problems.InvalidParameter("Name is required", "name");
var p400Range = Problems.InvalidParameter("Age must be greater than 18", "age");
// 422 Unprocessable Entity – regra de negócio violada
var p422 = Problems.ValidationFailed("Order total cannot be negative", "total");
var p422Combo = Problems.ValidationFailed("Payment method not compatible with plan", "paymentMethod");
// 403 Forbidden – não permitido
var p403 = Problems.NotAllowed("You do not have permission to cancel this order");
var p403Policy = Problems.NotAllowed("Action not allowed during maintenance window");
// 409 Conflict – estado inválido
var p409 = Problems.InvalidState("Order is already shipped");
var p409Lock = Problems.InvalidState("Resource is locked by another process");
// 404 Not Found – recurso inexistente
var p404 = Problems.NotFound("User not found", "userId");
var p404Filter = Problems.NotFound("No results for filter", "query");
// 500 Internal Server Error – erro inesperado
var p500 = Problems.InternalError(new Exception("Unexpected error"));
var p500Default = Problems.InternalError(); // usa mensagem padrão configurada
// Custom – descreva seu tipo em ProblemDetails
var pCustom = Problems.Custom("Order on hold", typeId: "order-on-hold", property: "status");InternalError inverte a ordem de property e typeId em relação às demais fábricas.
Não é um erro de compilação — é um erro silencioso que altera o type do ProblemDetails:
// Demais fábricas: (detail, property, typeId)
Problems.InvalidParameter(string detail, string? property = null, string? typeId = null);
Problems.ValidationFailed(string detail, string? property = null, string? typeId = null);
Problems.NotAllowed (string detail, string? property = null, string? typeId = null);
Problems.InvalidState (string detail, string? property = null, string? typeId = null);
Problems.NotFound (string detail, string? property = null, string? typeId = null);
// InternalError: (detail, typeId, property) <-- invertido!
Problems.InternalError (string? detail, string? typeId = null, string? property = null);
// Custom exige typeId, na segunda posição
Problems.Custom (string detail, string typeId, string? property = null);// ❌ define typeId = "userId" sem querer
Problems.InternalError("Falha ao gravar", "userId");
// ✅ sempre use argumento nomeado em InternalError
Problems.InternalError("Falha ao gravar", property: "userId");Extensões e propriedades encadeadas:
p400.With("attempt", 1).ChainProperty("User", 0); // User[0].name
p422.With("policy", "minimum-total");Quando for necessário atravessar uma fronteira que ainda espera exceptions, converta a coleção de problemas em exceção com ToException(...):
var ex = (p400 + p422).ToException("Validation errors: {0}");
throw ex;Para validar as propriedades de uma classe pode ser criado um método HasProblems que retorna os problemas encontrados:
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public bool HasProblems([NotNullWhen(true)] out Problems? problems)
{
Problems errors = [];
if (string.IsNullOrWhiteSpace(Name))
errors += Problems.InvalidParameter("Name is required", "name");
if (Age < 18)
errors += Problems.InvalidParameter("Age must be at least 18", "age");
if (errors.Count > 0)
{
problems = errors;
return true;
}
problems = null;
return false;
}
}DICA: Use a biblioteca RoyalCode.SmartValidation para validação fluente, com RuleSet, e integrada com Problems e Results.
Em ProblemDetails (RFC 9457), o campo type deve ser um identificador do tipo de problema (preferencialmente uma URI).
Recomendações atualizadas do RFC 9457:
- Use um
typeestável, único e documentado, de preferência uma URI absoluta (ex.:https://api.seu-dominio.com/problems/order-on-hold). - Inclua
titlehumano-legível estatuscoerente ao tipo descrito. Evite títulos genéricos. - Utilize
instance(URI) para identificar a ocorrência específica do problema quando aplicável. - As extensões devem usar nomes claros e estáveis; evite sobrescrever campos reservados (
type,title,status,detail,instance). - Evite
about:blankpara problemas customizados; descreva tipos próprios com documentação.
Impacto do Problems.Custom(detail, typeId, property) na conversão:
- O
typeIddoProblemé usado para localizar umaProblemDetailsDescriptionemProblemDetailsOptions.Descriptor. - Se a descrição tem
Typeexplícito, esse valor viraProblemDetails.Type. - Se a descrição não tem
Type, a URI é gerada porBaseAddress + TypeComplement + TypeId. ProblemDetails.TitleeProblemDetails.Statusvêm da descrição localizada.ProblemDetails.Detailvem dodetaildo problema ocorrido; jáProblemDetailsDescription.Descriptionserve para documentação/catálogo do tipo.- Sem descrição específica para o
typeId,CustomProblemcai no tipo genéricoproblem-occurred, com status padrão 400. Para contrato de API estável, sempre registre uma descrição para cadatypeIdcustomizado. - Quando há vários problemas customizados na mesma resposta, o tipo externo é agregado (
aggregate-problems-details) e os problemas específicos aparecem nos detalhes agregados.
Exemplo de configuração do tipo no ProblemDetailsOptions:
using System.Net;
using RoyalCode.SmartProblems.Descriptions;
var options = new ProblemDetailsOptions();
options.Descriptor.Add(new ProblemDetailsDescription(
typeId: "order-on-hold",
title: "Order on hold",
description: "The order cannot move forward while risk analysis is pending.",
status: HttpStatusCode.Conflict));
Problems problem = Problems.Custom("Order is on hold due to risk analysis", "order-on-hold");
var pd = problem.ToProblemDetails(options);
// Type: "tag:problemdetails/.problems#order-on-hold"
// Title: "Order on hold"
// Status: 409
// Detail: "Order is on hold due to risk analysis"Quando o contrato público exige uma URI absoluta, informe o type explicitamente:
options.Descriptor.Add(new ProblemDetailsDescription(
typeId: "order-on-hold",
type: "https://api.exemplo.com/problems/order-on-hold",
title: "Order on hold",
description: "The order cannot move forward while risk analysis is pending.",
status: HttpStatusCode.Conflict));Use o pacote RoyalCode.SmartProblems.ProblemDetails. Registre as descrições conhecidas pela aplicação com AddProblemDetailsDescriptions; elas alimentam a conversão para ProblemDetails e a página HTML de documentação.
using System.Net;
using RoyalCode.SmartProblems.Descriptions;
builder.Services.AddProblemDetailsDescriptions(options =>
{
options.BaseAddress = "https://api.exemplo.com/problems";
options.TypeComplement = "/";
options.Descriptor.Add(new ProblemDetailsDescription(
typeId: "order-on-hold",
title: "Order on hold",
description: "The order cannot move forward while risk analysis is pending.",
status: HttpStatusCode.Conflict));
});Também é possível carregar descrições por arquivo JSON:
builder.Services.AddProblemDetailsDescriptions(options =>
{
options.DescriptionFiles = ["problem-details.json"];
});Formato recomendado do arquivo:
[
{
"typeId": "order-on-hold",
"type": "https://api.exemplo.com/problems/order-on-hold",
"title": "Order on hold",
"description": "The order cannot move forward while risk analysis is pending.",
"status": 409
}
]Depois de registrar as descrições, publique a página de catálogo:
var app = builder.Build();
app.MapProblemDetailsDescriptionPage(); // GET /.problems
// ou:
app.MapProblemDetailsDescriptionPage("/docs/problems");A página é opt-in e lista os tipos conhecidos pelo ProblemDetailsDescriptor: categorias padrão, descrições carregadas em JSON e descrições adicionadas em código. Ela mostra TypeId, URI final do type, título, status e descrição. Se uma descrição não tiver Type explícito, a página resolve a URI com BaseAddress + TypeComplement + TypeId; quando BaseAddress estiver no default da biblioteca, a própria rota da página é usada como base navegável.
Regras para IA ao criar problemas customizados:
- Use
typeIdcurto, estável e válido como parte relativa de URI, preferencialmente em kebab-case (order-on-hold,payment-required). - Registre uma
ProblemDetailsDescriptionpara cadatypeIdcustomizado antes de expor a API. - Use
typeexplícito quando a documentação pública mora em uma URL canônica; useBaseAddress/TypeComplementquando a própria API publica o catálogo. - Escreva
descriptioncomo documentação do tipo: quando ocorre, por que ocorre e o que o consumidor pode fazer. - Não dependa do fallback
problem-occurredpara erros de domínio públicos.
Construção e verificação:
Result<string> ok = "Hello";
Result<string> fail = Problems.InvalidParameter("Invalid", "prop");
if (ok.HasValue(out var value)) { /* sucesso */ }
if (fail.HasProblems(out var errs)) { /* erros */ }Composição síncrona e assíncrona:
var res = ok.Map(v => v.Length); // Result<int>
var next = ok.Continue(v => Result.Ok()); // Result
var async = await ok.MapAsync(static v => Task.FromResult(v.Length));
// Async com TParam: param, delegate, ct por último.
var saved = await ok.ContinueAsync(
repository,
static async (value, repo, token) =>
{
await repo.SaveAsync(value, token);
},
ct);
// branch explícito: Match
var outRes = ok.Match(
value => Result.Ok(),
problems => problems.AsResult());
// branch assíncrono: MatchAsync
var outResAsync = await ok.MatchAsync(
value => Task.FromResult(Result.Ok()),
problems => Task.FromResult(problems.AsResult()));Regra para sobrecargas Async com TParam:
- Quando o delegate retorna
TaskouTask<T>, ele recebeCancellationTokencomo último parâmetro. - O
CancellationTokenpúblico do método fica por último e tem default:.MapAsync(param, static (..., token) => ..., ct). - Não use a forma antiga
.MapAsync(param, ct, delegate)ou.ContinueAsync(param, ct, delegate). - Delegates síncronos com
TParamcontinuam semCancellationToken.
Exemplo de MatchAsync com TParam e CancellationToken:
return await result.MatchAsync(
logger,
static (value, log, token) =>
{
token.ThrowIfCancellationRequested();
log.LogInformation("Operation succeeded");
return Task.FromResult(value);
},
static (problems, log, token) =>
{
token.ThrowIfCancellationRequested();
log.LogWarning("Operation failed with {Count} problems", problems.Count);
return Task.FromResult(string.Empty);
},
ct);Casos de uso reais (serviços, handlers, repositórios):
// Result sem valor
public readonly struct UserService
{
private readonly IUserRepository _repo;
private readonly AbstractValidator<UserInput> _validator; // EnsureIsValid é extensão de AbstractValidator<T>
private readonly IUserPolicy _policy;
public Result Create(UserInput input)
{
// validação de entrada
// Atenção: cada `out var` precisa de um nome único no mesmo escopo (CS0128).
if (input.HasProblems(out var inputProblems))
return inputProblems; // 400.
// regra de negócio
if (_validator.EnsureIsValid(input).HasProblems(out var validationProblems))
return validationProblems; // 400/422 etc.
// regra de negócio
if (!_policy.CanCreate(input))
return Problems.NotAllowed("Not allowed to create user");
// persistência
_repo.Add(input);
return Result.Ok();
}
public async Task<Result> DisableAsync(int id)
{
var findUser = await _repo.FindByIdAsync(id);
if (findUser.NotFound(out var problem))
return problem; // 404
findUser.Entity.Disable();
return Result.Ok();
}
}
// Result com valor
public readonly struct OrderService
{
private readonly IOrderRepository _repo;
public Result<Order> Get(int id)
{
var found = _repo.TryFind(id); // retorna FindResult<Order,int>
return found.ToResult();
}
}Por que Result favorece um ótimo tratamento de erros?
- Substitui exceções em fluxo esperado por um tipo explícito de sucesso/falha, tornando o controle de fluxo transparente.
- Padroniza mensagens e categorias via
Problems, permitindo conversão consistente paraProblemDetailsem APIs. - Facilita composição funcional (Map, Continue, Match), reduzindo boilerplate e melhorando legibilidade.
- Integra com validação (
FluentValidation) e persistência (EFFindResult).
Performance: Result é um readonly struct
- Structs evitam alocação de heap em cenários comuns e permitem passagem por valor eficiente.
readonlygarante imutabilidade e melhor otimização pelo JIT.- Métodos marcados com
AggressiveInliningreduzem overhead em chamadas frequentes. - Em pipelines síncronos/assíncronos curtos, reduz GC pressure em comparação com exceções.
Agregação de problemas:
Result<string> r1 = Problems.InvalidParameter("A");
Result r2 = Problems.InvalidParameter("B");
r1 += r2; // combina problemasA extensão de Entity Framework fornece métodos TryFindAsync, TryFindByAsync e FindByCriteria que retornam um FindResult<TEntity>.
Esse tipo encapsula o resultado da busca: a entidade encontrada (Entity) ou um problema padronizado quando não encontrada.
-
TryFindAsync(DbContext, Id<TEntity,TId>),TryFindAsync(DbSet<TEntity>, TId)eTryFindAsync(DbSet<TEntity>, Id<TEntity,TId>):- Quando a entidade não existe, gera um
Problemcom categoriaNotFound(HTTP 404) e mensagem bem definida. - Ao receber
Id<TEntity,TId>, o valor usado na busca e no problema éid.Value, não o wrapperId. - Campos extras adicionados em
Extensions:ideentity.
- Quando a entidade não existe, gera um
-
TryFindByAsync(DbContext/DbSet, Expression<Func<TEntity,bool>>):- Executa o filtro com
FirstOrDefaultAsync. - Se não encontrar, tenta gerar uma mensagem rica analisando o predicado.
- Só gera critérios automáticos para expressões
&&compostas por igualdades (==) em que um lado é membro direto da entidade (e => e.Name) e o outro lado é valor constante/capturado. - Qualquer expressão ambígua ou potencialmente enganosa degrada para a mensagem genérica
The record for 'Entity' was not found.
- Executa o filtro com
-
FindByCriteria():- Use quando a busca tem dois ou mais critérios, chave composta, filtros condicionais, ou quando você quer informar os valores diretamente sem depender da análise automática da expressão.
- Pode começar em
DbContext,DbSet<TEntity>ouIQueryable<TEntity>já customizado comInclude,AsNoTrackingetc. - Cada chamada
Byretorna uma nova instância; para montar filtros condicionais, reatribua a variável.
Uso típico por id:
Id<TestEntity, int> id = 4;
var entry = await db.TestEntities.TryFindAsync(id, ct);
if (entry.NotFound(out var problem))
{
// problem.Detail: "The record of 'The Entity for Tests' with id '4' was not found"
// problem.Extensions: { id: 4, entity: "TestEntity" }
return problem;
}Uso por propriedade simples:
var byName = await db.TestEntities.TryFindByAsync(e => e.Name == "Test4", ct);
if (byName.NotFound(out var problemByName))
{
// problemByName.Detail: "The record of 'The Entity for Tests' with Name 'Test4' was not found"
// problemByName.Extensions: { Name: "Test4", entity: "TestEntity" }
return problemByName;
}Busca composta recomendada com FindByCriteria:
var city = await db.FindByCriteria<City>()
.By(c => c.StateId, stateId)
.By(c => c.Name, name)
.TryFindAsync(ct);
if (city.NotFound(out var problem))
{
// Detail: "The record of 'City' with StateId '42', Name 'Blumenau' was not found"
// Extensions: { entity: "City", StateId: 42, Name: "Blumenau" }
return problem;
}Começando a partir de um IQueryable customizado:
var city = await db.Set<City>()
.AsNoTracking()
.Include(c => c.State)
.FindByCriteria()
.By(c => c.StateId, stateId, "State")
.By(c => c.Name, name)
.TryFindAsync(ct);Filtros condicionais: como FindCriteria<TEntity> é imutável, sempre reatribua:
var criteria = db.FindByCriteria<City>()
.By(c => c.StateId, stateId);
if (!string.IsNullOrWhiteSpace(name))
criteria = criteria.By(c => c.Name, name);
var city = await criteria.TryFindAsync(ct);Quando um critério precisa de lógica além de igualdade (StartsWith, range, OR), use a sobrecarga com predicado e dados explícitos para o problema:
var city = await db.FindByCriteria<City>()
.By(c => c.StateId, stateId)
.By(c => c.Name.StartsWith(prefix), byName: "Name", propertyName: "Name", value: prefix)
.TryFindAsync(ct);Também é possível criar um problema multi-critério diretamente com FindCriterion:
FindCriterion[] criteria =
[
new("StateId", stateId, "State"),
new("Name", name)
];
FindResult<City> notFound = FindResult<City>.Problem(criteria);Regras de FindCriterion e FindResult<TEntity>.Problem(criteria):
- Critérios são listados na ordem recebida.
- Com um único critério, a mensagem mantém o formato legado de
Problem(byName, propertyName, value). - Com múltiplos critérios, o detalhe lista todos:
State '42', Name 'Blumenau'. ByNamenulo ou em branco é resolvido porDisplayNames, respeitandoDisplayNameAttribute.FindCriterionrejeitapropertyNamenulo/vazio;default(FindCriterion)é ignorado pela fábrica multi-critério.- Se todos os critérios forem inválidos/ignorados, a mensagem cai para o
NotFoundgenérico. - Em
Extensions, se a mesmapropertyNameaparecer mais de uma vez, a última vence.
Sobre a análise automática de TryFindByAsync(predicate):
// Gera mensagem rica multi-critério:
await db.TryFindByAsync<City>(c => c.StateId == stateId && c.Name == name, ct);
// Também funciona com a igualdade invertida:
await db.TryFindByAsync<City>(c => name == c.Name, ct);
// Degrada para mensagem genérica: "!=" não afirma que a entidade tem aquele valor.
await db.TryFindByAsync<City>(c => c.Name != "Blumenau", ct);
// Degrada para genérica: OR não pode ser descrito como lista simples de critérios AND.
await db.TryFindByAsync<City>(c => c.Name == "A" || c.Name == "B", ct);
// Degrada para genérica: cadeia profunda ou comparação entre membros da entidade.
await db.TryFindByAsync<City>(c => c.State.Name == "SC", ct);
await db.TryFindByAsync<State>(s => s.Name == s.Code, ct);A análise do valor nunca compila a expressão nem invoca métodos do predicado, então track("x") não é chamado uma segunda vez apenas para montar a mensagem.
Ela pode, porém, ler novamente getters de objetos capturados, como request.Name, para obter o valor usado no detalhe. Se o getter tiver efeito colateral ou valor variável, prefira FindByCriteria().By(c => c.Name, request.Name) e capture o valor uma vez antes da busca.
Quando possui nomes customizados em uma chamada direta de TryFindByAsync, use a sobrecarga explícita:
var entry2 = await db.TryFindByAsync<TestEntity>(
e => e.Name == "Test4",
byName: "Name",
propertyName: "name",
propertyValue: "Test4",
ct);
if (entry2.NotFound(out var p))
{
// p.Extensions: { name: "Test4", entity: "TestEntity" }
}Persistência com helpers EF:
AddTo/AddToAsyncadiciona a entidade aoDbContextsomente quando oResult<TEntity>tem valor.SaveChanges/SaveChangesAsyncchamaDbContext.SaveChangessomente quando oResultestá em sucesso.RemoveFromAsyncexiste paraTask<FindResult<TEntity>>; remove somente quando a entidade foi encontrada e retornaResult<TEntity>.- Para
Result<TEntity>já materializado, prefiraAddTo(db)quando quiser encadear imediatamente comSaveChangesAsync.
Criação:
return await Product.Create(command)
.AddTo(db)
.SaveChangesAsync(db, ct);Criação quando a etapa anterior já é assíncrona:
return await CreateProductAsync(command, ct)
.AddToAsync(db, ct)
.SaveChangesAsync(db, ct);Remoção:
return await db.Products
.TryFindByAsync(p => p.Id == id, ct)
.RemoveFromAsync(db, ct)
.SaveChangesAsync(db, ct);Composição com FindResult:
var result = await entry.ContinueAsync(
repository,
static async (entity, repo, token) =>
{
await repo.SaveAsync(entity, token);
return Result.Ok();
},
ct);Com CollectAsync<TParam> o mesmo padrão vale: param, delegate, ct.
var result = await entry.CollectAsync(
dto,
static async (entity, request, token) =>
{
entity.Update(request.Name);
await Task.CompletedTask.WaitAsync(token);
},
ct);Não use a ordem antiga .CollectAsync(dto, ct, static ...); o CancellationToken do método deve ficar por último.
Além de NotFound, o FindResult também suporta retornar InvalidParameter em cenários onde o identificador/parâmetro informado é inválido para a operação atual.
Os métodos HasInvalidParameter(out problem, parameterName) e sobrecargas de Continue/Map/ToResult(parameterName) ajudam a padronizar essa resposta:
var res = entry.ToResult("id");
// Se o parâmetro "id" for inválido, retorna Problem InvalidParameter com detail e property padronizados.Quando a busca projeta a entidade para um DTO no provider, o FindResult carrega o tipo do DTO —
e as mensagens de problema nomeariam o DTO, não a entidade pesquisada. As factories ProjectedFrom
preservam a identidade da entidade original e geram NotFound e InvalidParameter lazily, com a
categoria correta e nomeando a entidade:
// busca por critérios (chave alternativa/composta) projetada para DTO
FindResult<ProductDetails> result = FindResult<ProductDetails>.ProjectedFrom<Product>(
dto, // ProductDetails? — null quando não encontrado
[new FindCriterion(nameof(Product.Sku), sku)]);
// busca por id projetada para DTO
FindResult<ProductDetails, int> byId = FindResult<ProductDetails, int>.ProjectedFrom<Product>(dto, id);
// mensagens nomeiam a entidade: "The record of 'Product' with Sku 'SKU-X' was not found"Critérios sem ByName têm o display name resolvido contra a entidade (respeitando [DisplayName]).
Não use um Problem pré-construído no lugar das factories: HasInvalidParameter devolveria o
problema armazenado com a categoria original (quirk documentado do construtor FindResult(Problem)).
FindCriteriaExtractor.Extract<TEntity>(filter) extrai FindCriterion[] de um predicado, um
critério por igualdade unida por && (e => e.Sku == sku && e.Region == region), em ordem de
declaração. A extração é tudo-ou-nada e nunca lança: qualquer construção não reconhecida
(||, !=, >, membro profundo, chamada de método, negação, etc.) degrada para um array vazio —
e a mensagem de problema resultante vira a genérica. É a mesma análise usada pelo TryFindByAsync
do pacote EF, agora reutilizável por outros consumidores (ex.: repositórios com projeção por predicado).
Os tipos OkMatch, NoContentMatch, CreatedMatch e AcceptedMatch permitem mapear Result/Result<T> para respostas HTTP padronizadas, convertendo automaticamente problemas em ProblemDetails (RFC 9457) quando necessário.
Exemplos baseados em MatchApi:
// POST: cria e retorna 201 com Location e corpo
private static async Task<CreatedMatch<PersonDetails>> CreatePerson(PersonCreate create)
{
await Task.Delay(10); // simulação
return _personService.CreatePerson(create)
.Map(person => new PersonDetails
{
Id = person.Id,
Name = person.Name,
Age = person.Age
})
.CreatedMatch(p => $"/api/match/{p.Id}");
}
// GET: retorna 200 com corpo ou 404 ProblemDetails
private static async Task<OkMatch<PersonDetails>> GetPerson(int id)
{
await Task.Delay(10);
return _personService.GetPerson(id)
.Map(person => new PersonDetails
{
Id = person.Id,
Name = person.Name,
Age = person.Age
});
}
// PATCH: retorna 200 OK ou ProblemDetails (400/404)
private static async Task<OkMatch> UpdatePersonName(int id, PersonUpdateName model)
{
await Task.Delay(10);
return _personService.UpdatePersonName(id, model);
}
// PATCH: retorna 200 OK ou ProblemDetails (400/404)
private static async Task<OkMatch> UpdatePersonAge(int id, PersonUpdateAge model)
{
await Task.Delay(10);
return _personService.UpdatePersonAge(id, model);
}
// DELETE: retorna 204 ou 404 ProblemDetails
private static async Task<NoContentMatch> DeletePerson(int id)
{
await Task.Delay(10);
return _personService.DeletePerson(id);
}Comportamento esperado (vide MatchApiTests):
- Sucesso: 201/200/204 com Location e/ou corpo conforme tipo.
- Falha: problemas convertidos para
ProblemDetailscom status coerente (404, 400, etc.).
AcceptedMatch e AcceptedMatch<T> respondem 202 Accepted: a solicitação foi aceita para
processamento, mas ele ainda não foi concluído (RFC 9110). Diferente de CreatedMatch, a
Location é opcional — quando informada, aponta um recurso para acompanhar o processamento.
Result produz 202 sem corpo; Result<T> produz 202 com T no corpo. Problemas nunca viram 202.
Exemplos baseados em AcceptedApi (vide AcceptedApiTests):
// POST: enfileira e responde 202 sem corpo e sem Location
private static async Task<AcceptedMatch> Queue(QueueRequest request)
{
await EnqueueAsync(request);
return Result.Ok();
}
// POST: 202 sem corpo, com Location fixa de acompanhamento
private static async Task<AcceptedMatch> QueueLocated(QueueRequest request)
{
Result result = await EnqueueAsync(request);
return result.AcceptedMatch("/api/accepted/status/fixed");
}
// POST: 202 com corpo (ticket) e Location derivada do valor
private static async Task<AcceptedMatch<TicketDetails>> TicketLocated(QueueRequest request)
{
Result<TicketDetails> result = await EnqueueTicketAsync(request);
return result.AcceptedMatch(t => $"/api/accepted/status/{t.Ticket}");
}Use WithExceptionFilter na borda HTTP para transformar exceções inesperadas em ProblemDetails 500 padronizado. Prefira aplicar o filtro em grupos, para que todas as rotas do grupo compartilhem a mesma política:
var group = app.MapGroup("/api")
.WithExceptionFilter();
group.MapGet("/produto/{id:int}", GetProduto);
group.MapPost("/produto", CriarProduto);Quando quiser registrar também falhas esperadas retornadas por OkMatch, CreatedMatch ou NoContentMatch, informe um LogLevel. O parâmetro loggerType define a categoria do logger usado pelo filtro:
var group = app.MapGroup("/api")
.WithExceptionFilter(LogLevel.Error, typeof(Program));
group.MapGet("/produto/{id:int}", GetProduto);Regras para IA ao gerar Minimal APIs:
- Use
WithExceptionFilter()emMapGroupquando várias rotas compartilham a mesma borda de API. - Use o filtro para exceptions inesperadas, falhas de infraestrutura e erros não previstos.
- Não use
try/catchnem exception filter para validação esperada, regra de domínio ou recurso não encontrado; retorneResult/Problemse converta comOkMatch,CreatedMatch,AcceptedMatchouNoContentMatch. - O filtro sempre registra exceptions capturadas como
LogLevel.Error. - O parâmetro
logLevelcontrola apenas o log de respostas de erro já modeladas comoMatchErrorResult.
Boas práticas (RFC 9457):
- Para
CreatedMatch, forneçaLocationcom URI absoluta ou relativa estável. - Use
AcceptedMatchsomente quando o processamento continua após a resposta; para operação síncrona concluída, useOkMatch/CreatedMatch/NoContentMatch. - Títulos (
title) claros e condizentes com otype; descrição (detail) objetiva. - Use
instancequando aplicável para identificar o recurso/ocorrência.
Métodos HttpResultExtensions.ToResultAsync desserializam respostas HTTP em Result/Result<T>:
- Em sucesso (2xx): retornam
Result.Ok()ouResult<T>com o corpo JSON. - Em falha (4xx/5xx): lê
application/problem+jsone converte paraProblems; se não for ProblemDetails, tenta texto puro ou leitor customizado.
Assinaturas principais:
Task<Result> ToResultAsync(this HttpResponseMessage response, CancellationToken ct = default);
Task<Result<T>> ToResultAsync<T>(this HttpResponseMessage response, JsonSerializerOptions? options = null, CancellationToken ct = default);
Task<Result<T>> ToResultAsync<T>(this HttpResponseMessage response, JsonTypeInfo<T> jsonTypeInfo, CancellationToken ct = default);
// Com FailureTypeReader para conteúdo de erro não-ProblemDetails
Task<Result<T>> ToResultAsync<T>(this HttpResponseMessage response, FailureTypeReader? failureTypeReader, JsonSerializerOptions? options = null, CancellationToken ct = default);
Task<Result<T>> ToResultAsync<T>(this HttpResponseMessage response, FailureTypeReader? failureTypeReader, JsonTypeInfo<T> jsonTypeInfo, CancellationToken ct = default);Exemplos reais de consumo com HttpClient:
var http = new HttpClient { BaseAddress = new Uri("https://api.exemplo.com") };
// 1) GET com corpo: sucesso → Result<T>, falha → Problems
var respGet = await http.GetAsync("/users/123");
var userResult = await respGet.ToResultAsync<UserDto>();
if (userResult.HasValue(out var user))
{
Console.WriteLine($"User: {user.Name}");
}
else if (userResult.HasProblems(out var problems))
{
// exibir problem details
foreach (var p in problems) Console.WriteLine($"{p.Category}: {p.Detail}");
}
// 2) POST criação: sucesso (201) sem corpo → Result.Ok(), Location em headers
var createResp = await http.PostAsJsonAsync("/users", new { name = "John", age = 20 });
var createResult = await createResp.ToResultAsync();
if (createResult.IsSuccess)
{
if (createResp.Headers.Location is Uri loc)
Console.WriteLine($"Criado em: {loc}");
}
else if (createResult.HasProblems(out var problems))
{
// entrada inválida (400) ou regra semântica (422)
foreach (var p in problems) Console.WriteLine($"Erro: {p.Property} → {p.Detail}");
}
// 3) PATCH atualização: sucesso (200) sem corpo, falha padronizada
var patchResp = await http.PatchAsJsonAsync("/users/123/name", new { name = "Mary" });
var patchResult = await patchResp.ToResultAsync();
if (!patchResult.IsSuccess && patchResult.HasProblems(out var errs))
{
// erros como NotFound(404) ou InvalidParameter(400)
foreach (var p in errs) Console.WriteLine($"{p.Category}: {p.Detail}");
}
// 4) GET lista com `JsonTypeInfo` otimizado
var respList = await http.GetAsync("/users");
var listResult = await respList.ToResultAsync(UsersContext.Default.ListUserDto);
if (listResult.HasValue(out var users))
{
Console.WriteLine($"Total: {users.Count}");
}
// 5) Falha com conteúdo não-ProblemDetails usando FailureTypeReader
var reader = new FailureTypeReader(async r =>
{
var text = await r.Content.ReadAsStringAsync();
return new FailureTypeReaderResult(true, Problems.InternalError(text));
});
var respOther = await http.GetAsync("/external/service");
var otherResult = await respOther.ToResultAsync(reader);
if (otherResult.HasProblems(out var ps))
{
foreach (var p in ps) Console.WriteLine(p.Detail);
}Boas práticas (RFC 9457):
- APIs devem retornar
application/problem+jsonpara falhas; clientes devem interpretartype,title,status,detail,instance. - Use
type/instanceURIs estáveis; evite conflitar extensões com campos reservados.
Armadilhas que o IntelliSense não revela. Todas verificadas contra 1.0.0-preview-7.0.
TryFindAsync(id) usa FindAsync do EF: consulta o change tracker primeiro e, se a entidade já
estiver rastreada, retorna sem emitir SQL. TryFindByAsync(predicado) e FindByCriteria()...TryFindAsync(ct)
usam FirstOrDefaultAsync: sempre emitem SQL, e alterações ainda não salvas não afetam o filtro.
// ✅ chave primária: pode resolver pelo change tracker, sem ida ao banco
var byId = await db.Set<City>().TryFindAsync(cityId, ct);
// ⚠️ sempre emite SQL; não enxerga entidades adicionadas/alteradas e ainda não salvas
var byName = await db.FindByCriteria<City>().By(c => c.Name, "Nova").TryFindAsync(ct);Regra: busca por chave primária → TryFindAsync(id). Busca por outros campos → TryFindByAsync /
FindByCriteria, assumindo roundtrip.
O seletor precisa acessar uma propriedade do parâmetro da lambda. Qualquer outra coisa lança
ArgumentException em tempo de execução.
// ❌ ArgumentException: não é membro do parâmetro `c`
criteria.By(c => outroObjeto.Nome, valor);
// ❌ ArgumentException: cadeia profunda (o display name seria resolvido no tipo errado)
criteria.By(c => c.State.Name, "SC");
// ✅ membro direto
criteria.By(c => c.Name, valor);
// ✅ para navegar ou usar lógica além de igualdade, use a sobrecarga de predicado
criteria.By(c => c.State.Name == "SC", byName: "State", propertyName: "stateName", value: "SC");Cuidado: isto compila. Como existe conversão implícita de int para Id<State,int>, o compilador
infere TValue = Id<State,int> e insere um Convert no seletor. O erro só aparece em tempo de execução,
como ArgumentException do próprio builder.
Id<State, int> stateId = 42;
// ❌ compila, mas lança ArgumentException em tempo de execução:
// "Cannot filter 'StateId' (of type Int32) by an Id<,> wrapper. Pass the underlying value instead..."
db.FindByCriteria<City>().By(c => c.StateId, stateId);
// ✅ use o valor
db.FindByCriteria<City>().By(c => c.StateId, stateId.Value);Em TryFindAsync, ao contrário, o wrapper é aceito nas três sobrecargas e o id.Value é usado
internamente — tanto em db.TryFindAsync(id, ct) quanto em db.Set<T>().TryFindAsync(id, ct).
FindCriteria<T> é struct e detecta o uso não inicializado, lançando InvalidOperationException
com mensagem explicativa em By e em TryFindAsync. Sempre comece por FindByCriteria(...).
Result<T> não tem essa guarda: o default se comporta como sucesso com valor nulo.
// ❌ IsSuccess == true e HasValue devolve true com value == null
Result<Order> r = default;
if (r.HasValue(out var order)) { order.Total(); /* NullReferenceException */ }
// ✅ construa explicitamente
Result<Order> ok = order;
Result<Order> fail = Problems.NotFound("Order not found", "orderId");Nunca declare Result<T> sem inicializar, nem use new Result<T>() sem argumentos.
Além de HasProblems/HasValue, existem duas formas que evitam checagem dupla — e uma que lança
exceção, contrariando a filosofia da biblioteca se usada no fluxo esperado.
// ✅ um único teste, devolve problemas OU valor
if (result.HasProblemsOrGetValue(out var problems, out var order))
return problems;
// aqui `order` não é nulo
// ✅ variação com a ordem invertida
if (result.HasValueOrGetProblems(out var value, out var errors)) { /* sucesso */ }
// ⚠️ EnsureHasValue LANÇA InvalidOperationException se houver problemas.
// Use apenas quando a falha já foi tratada antes e é logicamente impossível aqui.
result.EnsureHasValue(out var entity);
// ❌ EnsureHasValue não protege contra o `default`: ele só lança quando IsFailure é true.
// Em default(Result<T>) não há problemas, então `bad` volta nulo silenciosamente (ver §8.4).
Result<Order> o = default;
o.EnsureHasValue(out var bad); // bad == null, sem exceçãoVer §3: InternalError é (detail, typeId, property), invertido em relação às demais fábricas.
Sempre passe property: nomeado.
HasProblems(out var problems) duas vezes no mesmo método é erro de compilação (CS0128).
Dê nomes distintos: inputProblems, validationProblems.
- Padronize categorias e status HTTP:
- 404 NotFound, 400 InvalidParameter (entrada), 422 ValidationFailed (semântica), 403 NotAllowed, 409 InvalidState, 500 Internal.
- Defina tipos customizados com
Problems.Custome descreva emProblemDetailsOptions.
- Siga o RFC 9457:
- Prefira URIs absolutas para
typeeinstance, títulos claros (title) estatuscoerente. - Não sobrescreva campos reservados; use
Extensionscom nomes estáveis e significativos.
- Prefira URIs absolutas para
- Use
Result/Result<T>como fluxo de sucesso/falha:- Componha com
Map,Continue,Match/MatchAsyncpara reduzir boilerplate. - Em sobrecargas
AsynccomTParame delegateTask, passeparam, depois o delegate, eCancellationTokenpor último. - Evite exceções para casos esperados; retorne problemas nas falhas.
- Componha com
- Valide entrada e regras de domínio:
- Modelo com
HasProblems(out Problems?)ou FluentValidation (EnsureIsValid,ToProblems). - Em APIs, converta problemas para
ProblemDetailsautomaticamente viaOkMatch/CreatedMatch/NoContentMatch.
- Modelo com
- Documente problemas expostos pela API:
- Registre descrições com
AddProblemDetailsDescriptions, em código ou JSON. - Publique
MapProblemDetailsDescriptionPage()quando consumidores precisarem consultar o catálogo de tipos. - Para
Problems.Custom, garanta que todotypeIdpúblico tenhatitle,description,statuse URI detypeestáveis.
- Registre descrições com
- Persistência e buscas:
- Use
TryFindAsync/TryFindByAsync(EF) e trateFindResultcomNotFound/HasInvalidParameter/ToResult([param]). - Para buscas com dois ou mais campos, chave composta ou filtros condicionais, prefira
FindByCriteria().By(...).TryFindAsync(ct). - Para mensagens
NotFoundmulti-critério fora do EF, useFindCriterioneFindResult<TEntity>.Problem(criteria). - Propague campos extras (
id,entity,property/value) emExtensionspara rastreabilidade.
- Use
- Cliente HTTP:
- Consuma com
ToResultAsync(valor ou problemas) e trateapplication/problem+jsoncorretamente. - Para erros não-ProblemDetails, considere
FailureTypeReader.
- Consuma com
- Observabilidade e contexto:
- Use
With(key, value)para anexar dados relevantes (ids, política aplicada, limites). - Encadeie propriedades com
ChainProperty(parent[, index])para apontar origem precisa.
- Use
- Performance:
Resultéreadonly struct; aproveite composição leve e evite alocações desnecessárias.
- Assinaturas:
- Consulte a documentação XML das libs no IDE/pacote para confirmar overloads, nomes de parâmetros e tipos de retorno antes de gerar código em APIs menos usadas.
SmartProblems fornece uma abordagem uniforme e eficiente para tratar sucesso e falha em operações .NET.
Com Problem/Problems você modela erros com categorias e contexto; com Result/Result<T> você compõe fluxos sem lançar exceções em casos esperados.
A biblioteca integra-se a APIs via ProblemDetails (RFC 9457), ao EF via FindResult/TryFind* e ao cliente HTTP com ToResultAsync.
Tipos como OkMatch, CreatedMatch, AcceptedMatch e NoContentMatch simplificam respostas HTTP consistentes.
O uso de readonly struct e APIs inlinadas favorece performance, e as extensões (With, ChainProperty) melhoram rastreabilidade.
Diretrizes de geração alinhadas às seções 1–8. Antes de gerar código, leia a §1.1 (pacote e using)
e a §8 (erros comuns) — juntas elas cobrem os erros que não aparecem no IntelliSense.
- Pacotes e
using- Resolva o
usingpela tabela da §1.1; pacote e namespace divergem (OkMatch→ pacoteApiResults, tipos emHttpResults, extensions emMicrosoft.AspNetCore.Http;ToResultAsync→ pacoteHttp, namespaceSystem.Net.Http;ToProblemDetails→ pacoteProblemDetails, namespace...Conversions). - Extensões de EF e ASP.NET Core vivem em namespaces da Microsoft: instalado o pacote, não há
usingnovo.
- Resolva o
- Armadilhas obrigatórias (§8)
Problems.InternalErroré(detail, typeId, property), invertido em relação às demais fábricas: sempre passeproperty:nomeado.- Nunca produza
default(Result<T>)nemnew Result<T>(): reporta sucesso com valor nulo. - Em
FindCriteria.By, o seletor deve ser membro direto do parâmetro, e o valor deve ser o valor cru (id.Value), nunca umId<,>. - Cada
out varprecisa de nome único no escopo (CS0128).
- Problemas e categorias
- Gere problemas com a categoria correta:
InvalidParameter(400 entrada),ValidationFailed(422 semântica),NotFound(404),InvalidState(409),NotAllowed(403),InternalServerError(500),CustomProblem(typeId descrito). - Use
Propertypara apontar o campo eWith(key, value)para contexto adicional;ChainProperty(parent[, index])para caminhos.
- Gere problemas com a categoria correta:
- Resultados e composição
- Retorne
Result/Result<T>em serviços/handlers; componha comMap,Continue,MatcheMatchAsync. - Para métodos
AsynccomTParame callbacks que retornamTask/Task<T>, gere chamadas no formatoMetodoAsync(param, static (..., token) => ..., ct). - Evite exceções para fluxos esperados; converta para problemas e propague via
Result.
- Retorne
- APIs Web (servidor)
- Converta
Result/Result<T>emOkMatch,CreatedMatch(comLocation),AcceptedMatch(202,Locationopcional) eNoContentMatch. - Para Minimal APIs, prefira
app.MapGroup("/group-route").WithExceptionFilter()e mapeie as rotas no grupo; use o filtro apenas para exceptions inesperadas. - Configure
ProblemDetailsOptions, registre descrições comAddProblemDetailsDescriptionse exponhaMapProblemDetailsDescriptionPage()quando a API deve documentar seus tipos. - Descreva todo
typeIdcustomizado; respeite RFC 9457 (type,title,status,detail,instance).
- Converta
- Entity Framework
- Use
TryFindAsync/TryFindByAsyncpara obterFindResult; converta paraResultcomToResult([param]). - Para
Id<TEntity,TId>, pode chamar tantodb.TryFindAsync(id, ct)quantodb.Set<TEntity>().TryFindAsync(id, ct); o valor real usado éid.Value. - Para busca por um campo simples, prefira
TryFindByAsync(e => e.Property == value, ct)ou a sobrecarga de seletorTryFindByAsync(e => e.Property, value, ct). - Para dois ou mais critérios, chave composta ou filtros condicionais, gere
db.FindByCriteria<TEntity>().By(...).By(...).TryFindAsync(ct). - Lembre que
FindCriteria<TEntity>.Byretorna nova instância; em filtros condicionais, reatribuacriteria = criteria.By(...). - Use a sobrecarga
By(predicate, byName, propertyName, value)quando o critério tiverStartsWith, range,ORou outra lógica que não seja igualdade simples. - Ao gerar mensagens manuais de não encontrado com múltiplos campos, use
FindCriterion[]eFindResult<TEntity>.Problem(criteria). - Não tente documentar
!=,>,<,||, membro profundo (e.State.Name) ou comparação membro-a-membro (e.A == e.B) como se fossem critériosANDsimples; nesses casos oTryFindByAsync(predicate)degrada paraNotFoundgenérico ou deve receber dados explícitos. - Ao não encontrar, retorne
NotFoundpadronizado comExtensions(id,entity,property/value).
- Use
- Cliente HTTP
- Consuma com
ToResultAsync(valor ou problemas); trateapplication/problem+jsone useFailureTypeReaderpara conteúdos não-ProblemDetails.
- Consuma com
- Validação
- Implemente
HasProblems(out Problems?)ou use FluentValidation (EnsureIsValid,ToProblems) para criarProblems.
- Implemente
- Performance e observabilidade
- Prefira
Result(readonly struct) para menor alocação; useWith/Extensionspara dados de diagnóstico.
- Prefira
Padrões de prompt para Agentes:
- “Implemente um serviço que valide entrada com FluentValidation, retorne
Resulte mapeie paraCreatedMatchcom Location.” - “Crie uma consulta EF com
TryFindByAsyncporName; retorneOkMatch<T>quando encontrado eProblemDetails 404quando não.” - “Crie uma busca EF composta com
FindByCriteria: filtre porStateIdeName, retorneFindResult<City>e documente oNotFoundcom os dois critérios.” - “Compose um
Result<Order>emResult<OrderDto>usandoMape trate falhas comMatch→Problems.AsResult().” - “Defina um
Problems.CustomcomtypeIde configureProblemDetailsOptionsseguindo RFC 9457 (URI absoluta emtype).” - “Consuma um endpoint com
HttpClienteToResultAsync<T>; em falha, itereProblemse exibacategory/detail.”