diff --git a/RoyalCode.EnterprisePatterns/.docs/domain.md b/RoyalCode.EnterprisePatterns/.docs/domain.md index e5362be..bdd0478 100644 --- a/RoyalCode.EnterprisePatterns/.docs/domain.md +++ b/RoyalCode.EnterprisePatterns/.docs/domain.md @@ -1,57 +1,82 @@ -# RoyalCode Enterprise Patterns – Domínio (Entities e Aggregates) +# Documentação da API de Domínio (Entities, DomainEvents, Aggregates) -Este documento descreve as bibliotecas de domínio e foca em como modelar entidades e agregados, separando o conteúdo de persistência (WorkContext/UnitOfWork/Repositories), que está documentado em `.docs/workcontext.md`. +Esta documentação apresenta os conceitos, funcionalidades e exemplos práticos para usar as bibliotecas de domínio nesta solução. +Serve também como referência para ferramentas de IA (ex.: GitHub Copilot) compreenderem e gerarem código correto com base na API das bibliotecas. + +Projetos alvo: .NET 8, .NET 9 e .NET 10. Escopo: - `RoyalCode.Entities` +- `RoyalCode.DomainEvents` - `RoyalCode.Aggregates` -Compatibilidade: múltiplos targets (.NET 8, .NET 9 e .NET 10) via `$(LibTargets)`. +Para persistência (WorkContext/UnitOfWork/Repositories), consulte `.docs/workcontext.md`. + +## 1. Introdução + +Os componentes de domínio fornecem abstrações simples e consistentes para modelar entidades com identificadores, +códigos de negócio, estados, e para compor agregados que coletam eventos de domínio. +As APIs são minimalistas, focadas em segurança (setters protegidos), imutabilidade e integração com camadas de infraestrutura de publicação de eventos. + +Benefícios principais: +- Entidades tipadas por ID com base reutilizável. +- Suporte a códigos de negócio (`Code`) distintos do ID. +- Contratos para estados (`IsActive`) e exclusão lógica (`IsDeleted`). +- Coleção de eventos de domínio em agregados com observadores. +- APIs simples e neutras de infraestrutura, prontas para integração. --- -## RoyalCode.Entities +## 2. RoyalCode.Entities -Fundação para modelagem de entidades. Fornece contratos e implementações básicas para ID, Code, Guid, estado ativo e exclusão lógica. +Fundação para modelagem de entidades. Fornece contratos e implementações básicas para `Id`, `Code`, `Guid`, estado ativo e exclusão lógica. -Principais tipos +Principais tipos: - `IEntity` / `IEntity`: marca uma entidade e o tipo do seu identificador. - `Entity`: base com propriedade `Id` (set protegido). -- `Entity`: base com `Id` e `Code`. +- `Entity`: base com `Id` e `Code` (set protegido). - `IHasId`: expõe `Id` para entidades/DTOs. - `IHasCode`: expõe `Code` (identificador amigável e único, distinto do ID). - `IHasGuid`: expõe `Guid` global (não substitui o ID; útil para referência cruzada entre bancos/contextos). - `IActiveState`: expõe `IsActive` para habilitar/desabilitar sem deletar. - `ISoftDeletable`: expõe `IsDeleted` para exclusão lógica. -Referência rápida de API (como Copilot deve sugerir) -- Criar entidade: `public class Product : Entity { /* propriedades */ }` -- Criar DTO associado: `public class ProductDto : IHasId { public Guid Id {get;set;} /* ... */ }` -- Entidade com código: `public class Sku : Entity { /* Code já incluído */ }` +Assinaturas relevantes (resumo): +- `public abstract class Entity : IEntity { public TId Id { get; protected set; } }` +- `public abstract class Entity : Entity, IHasCode { public TCode Code { get; protected set; } }` +- `public interface IHasGuid { Guid Guid { get; } }` +- `public interface IActiveState { bool IsActive { get; } }` +- `public interface ISoftDeletable { bool IsDeleted { get; } }` -Quando usar +Quando usar: - Herde de `Entity` para qualquer entidade de domínio com ID. - Use `Entity` quando existir também um código de negócio único. - Implemente `IHasGuid`, `IActiveState` e/ou `ISoftDeletable` conforme os requisitos do domínio. -Exemplo básico +Exemplo básico: ```csharp using RoyalCode.Entities; public class Person : Entity { - public string Name { get; set; } = null!; + public string Name { get; private set; } = string.Empty; + + public Person(int id, string name) + { + Id = id; + Name = name; + } } ``` -Exemplo com `Entity` +Exemplo com `Entity`: ```csharp using RoyalCode.Entities; public class CatalogItem : Entity { - public string Name { get; set; } = null!; - // Code é herdado e tem set protegido + public string Name { get; private set; } = string.Empty; + public CatalogItem(Guid id, string code, string name) { Id = id; @@ -61,26 +86,79 @@ public class CatalogItem : Entity } ``` +DTO associado usando `IHasId`: +```csharp +using RoyalCode.Entities; + +public class PersonDto : IHasId +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} +``` + --- -## RoyalCode.Aggregates +## 3. RoyalCode.DomainEvents + +Modelagem de eventos de domínio e sua coleção observável. Integrado a agregados para registrar mudanças relevantes do domínio. + +Principais tipos: +- `IDomainEvent` (herda `IHasId`): evento com `Id` e `Occurred` (`DateTimeOffset`). +- `DomainEventBase`: base abstrata que gera `Id` e `Occurred` automaticamente (ou permite definir em sobrecarga para desserialização). +- `IDomainEventCollection` (`ICollection`): coleção com `Observe(Action)` e `RemoveObserver(...)`. +- `DomainEventCollection`: implementação padrão com observadores disparados a cada `Add` e reexecução para eventos já acumulados. +- `IHasEvents`: contrato para entidades que expõem `IDomainEventCollection? DomainEvents { get; set; }`. + +Assinaturas relevantes (resumo): +- `public interface IDomainEvent : IHasId { DateTimeOffset Occurred { get; } }` +- `public abstract class DomainEventBase : IDomainEvent { public Guid Id { get; } public DateTimeOffset Occurred { get; } }` +- `public interface IDomainEventCollection : ICollection { void Observe(Action); void RemoveObserver(Action); }` + +Exemplo de evento de domínio: +```csharp +using RoyalCode.DomainEvents; + +public sealed class OrderCreated : DomainEventBase +{ + public Guid OrderId { get; } + public string Number { get; } + + public OrderCreated(Guid orderId, string number) + { + OrderId = orderId; + Number = number; + } +} +``` + +Observando eventos já criados e futuros: +```csharp +var collection = new DomainEventCollection(); +collection.Observe(evt => Console.WriteLine($"Observed: {evt.GetType().Name} at {evt.Occurred}")); + +collection.Add(new OrderCreated(Guid.NewGuid(), "N-123")); +``` + +--- + +## 4. RoyalCode.Aggregates Modelagem de Agregados (DDD). Define a raiz do agregado e integra a coleta de eventos de domínio. -Principais tipos +Principais tipos: - `IAggregateRoot` / `IAggregateRoot`: marca a raiz do agregado e o tipo do ID. - `AggregateRoot`: base que herda de `Entity` e inclui `IDomainEventCollection? DomainEvents` + método protegido `AddEvent(IDomainEvent)`. - `AggregateRoot`: versão com `Code` além do ID e dos eventos. -Eventos de domínio -- `AggregateRoot` mantém uma coleção de eventos (`DomainEvents`). -- Use `AddEvent(evt)` para registrar eventos quando invariantes do agregado forem alteradas. -- O despacho/persistência de eventos é responsabilidade das bibliotecas de infraestrutura; aqui apenas coletamos os eventos. +Assinaturas relevantes (resumo): +- `public abstract class AggregateRoot : Entity, IAggregateRoot { protected void AddEvent(IDomainEvent evt) { DomainEvents ??= []; DomainEvents.Add(evt); } }` +- `public abstract class AggregateRoot : AggregateRoot, IHasCode { public TCode Code { get; protected set; } }` -Exemplo +Exemplo simples: ```csharp using RoyalCode.Aggregates; -using RoyalCode.DomainEvents; // IDomainEvent +using RoyalCode.DomainEvents; public sealed class Order : AggregateRoot { @@ -94,10 +172,15 @@ public sealed class Order : AggregateRoot } } -public record OrderCreated(Guid OrderId, string Number) : IDomainEvent; +public sealed class OrderCreated : DomainEventBase +{ + public Guid OrderId { get; } + public string Number { get; } + public OrderCreated(Guid orderId, string number) { OrderId = orderId; Number = number; } +} ``` -Exemplo com `AggregateRoot` +Exemplo com `AggregateRoot`: ```csharp using RoyalCode.Aggregates; using RoyalCode.DomainEvents; @@ -115,16 +198,107 @@ public sealed class ProductAggregate : AggregateRoot } } -public record ProductCreated(Guid ProductId, string Code) : IDomainEvent; +public sealed class ProductCreated : DomainEventBase +{ + public Guid ProductId { get; } + public string Code { get; } + public ProductCreated(Guid productId, string code) { ProductId = productId; Code = code; } +} ``` --- -## Boas práticas de modelagem -- Mantenha invariantes do agregado dentro da raiz (`AggregateRoot`) e dispare eventos com `AddEvent` após mudanças significativas. -- Não exponha `set` público para `Id`/`Code`; proteja modificações via comportamentos. -- Utilize `IHasGuid` quando precisar correlacionar a mesma entidade em múltiplos bancos/contextos. +## 5. Integração com Persistência e Publicação de Eventos + +- `DomainEvents` é apenas a coleção; a publicação/dispatch é responsabilidade de infraestrutura (WorkContext/UnitOfWork/Outbox). +- Em contextos EF, bibliotecas deste repositório acoplam observers e despacham eventos após `SaveChanges`. +- Consulte `.docs/workcontext.md` e projetos `RoyalCode.WorkContext.*`, `RoyalCode.UnitOfWork.*` e `RoyalCode.Events.Outbox.*`. + +--- + +## 6. Boas Práticas + +- Mantenha invariantes do agregado dentro da raiz e dispare eventos com `AddEvent` após mudanças significativas. +- Não exponha `set` público para `Id`/`Code`; use construtores/métodos de domínio. +- `IHasGuid` para correlação entre bancos/contextos. - Prefira `ISoftDeletable` e `IActiveState` para cenários de (des)ativação e exclusão lógica. +- Evite lógica de publicação de eventos dentro do agregado; mantenha coleta apenas. +- Eventos devem ser pequenos, descritivos e serializáveis quando necessário. + +--- + +## 7. Referência de API + +Entities: +- `IEntity`, `IEntity`, `Entity`, `Entity`, `IHasId`, `IHasCode`, `IHasGuid`, `IActiveState`, `ISoftDeletable`. + +DomainEvents: +- `IDomainEvent`, `DomainEventBase`, `IDomainEventCollection`, `DomainEventCollection`, `IHasEvents`. + +Aggregates: +- `IAggregateRoot`, `IAggregateRoot`, `AggregateRoot`, `AggregateRoot`. + +--- + +## 8. Exemplos de uso em Serviços + +Criando e coletando eventos ao modificar estado: +```csharp +public sealed class OrderService +{ + public Order Create(string number) + { + return new Order(number); // Order cria OrderCreated via AddEvent + } +} +``` + +Desabilitando uma entidade usando `IActiveState`: +```csharp +public sealed class Feature : Entity, IActiveState +{ + public bool IsActive { get; private set; } = true; + public void Disable() => IsActive = false; +} +``` + +Exclusão lógica com `ISoftDeletable`: +```csharp +public sealed class Document : Entity, ISoftDeletable +{ + public bool IsDeleted { get; private set; } + public void SoftDelete() => IsDeleted = true; +} +``` + +--- + +## 9. Resumo + +As bibliotecas de domínio fornecem uma base enxuta e consistente para modelar entidades e agregados em DDD, com ID e `Code`, estados (`IsActive`, `IsDeleted`) e coleta de eventos de domínio em `DomainEvents`. Os agregados usam `AddEvent` para registrar eventos e delegam despacho à infraestrutura. As APIs são simples, tipadas e compatíveis com múltiplos targets (.NET 8/9/10). + +--- + +## 10. Instruções para Ferramentas de IA (GitHub Copilot) + +Diretrizes de geração alinhadas às seções anteriores: +- Entidades + - Gere classes que herdam de `Entity` e, se necessário, `Entity`. + - Não use setters públicos para `Id`/`Code`; atribua via construtor ou métodos internos. + - Para DTOs, implemente `IHasId`. +- Agregados e Eventos + - Raiz do agregado deve herdar de `AggregateRoot` (ou ``). + - Registre eventos com `AddEvent(new MyEvent(...))`; eventos podem herdar `DomainEventBase`. + - Não implemente publicação dentro do agregado; use a coleção `DomainEvents` e deixe a infraestrutura publicar. +- Integração com Persistência + - Deixe `DomainEvents` serem processados por WorkContext/UnitOfWork/Outbox após `SaveChanges`. +- Padrões de prompt + - “Crie uma entidade com `Entity` e agregue evento com `AggregateRoot` ao criar.” + - “Implemente um evento herdando `DomainEventBase` com campos necessários e registre no agregado.” + - “Modele `ISoftDeletable` e `IActiveState` em entidades e use métodos para alterar estado sem expor setters.” -Para integração com persistência, consulte `.docs/workcontext.md`. +Evite +- Setters públicos em `Id`/`Code`. +- Publicar eventos diretamente de dentro das entidades. +- Misturar persistência/infraestrutura no domínio; mantenha coleções e contratos. diff --git a/RoyalCode.EnterprisePatterns/.docs/problems.md b/RoyalCode.EnterprisePatterns/.docs/problems.md new file mode 100644 index 0000000..b07fba6 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/.docs/problems.md @@ -0,0 +1,573 @@ +# Documentação da API SmartProblems (Problems, Result, FindResult) + +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. + +## 1. Introdução + +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 de `Problem` (encadeável, iterável, conversível para `Result`). +- `Result` / `Result`: resultado de operação (sucesso/falha), com APIs de composição/transformação. +- `FindResult` / `FindResult`: resultado de busca, com utilitários para continuar/mapear e converter para `Result`. +- Conversões para `ProblemDetails` (RFC 9457) para uso em APIs. +- Extensões para Entity Framework (métodos `TryFind*`). + +## 2. Funcionalidades Principais + +- 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)`. + +- Coleção de erros (`Problems`) + - Operadores: implícito de `Problem` para `Problems`, `+` para agregar problemas. + - Iteração, indexador, `Contains`, `CopyTo`, `Count`. + - Conversão para `Result` e para `InvalidOperationException` (`ToException(...)`). + +- Resultados (`Result`, `Result`) + - 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`, variantes `Async`. + - Soma de problemas entre resultados (`+=`). + +- Busca segura (`FindResult`, `FindResult`) + - Avaliação: `Found`, `NotFound(out problem)`, `HasInvalidParameter(...)`. + - Composição: `Collect`, `Continue`, `Map` (+ `Async`). + - Conversão: `ToResult([parameterName])`. + - Fábrica: `FindResult.Problem(byName, propertyName, propertyValue)`. + +- Conversão para `ProblemDetails` + - `RoyalCode.SmartProblems.Conversions`: `Problems.ToProblemDetails(options)`. + - `ProblemDetailsExtended` agrega múltiplos problemas (`errors`, `not_found`, `inner_details`). + - Personalização via `ProblemDetailsOptions` e `ProblemDetailsDescriptor`. + +- Integrações + - Entity Framework: `SmartProblemsEFExtensions` com `TryFindAsync`/`TryFindByAsync` e `FindResult`. + - FluentValidation: `ValidationsExtensions` (`ToProblems`, `HasProblems`, `EnsureIsValid`, `Validate`/`ValidateAsync`). + - HTTP/ASP.NET: utilitários para converter para `ProblemDetails` e resultados de API. + +- Tratamento de exceções + - `Problems.InternalError(Exception?, ExceptionOptions?)` com controle de mensagem, tipo e stack trace. + - `Problems.ExceptionHandler` para mapear exceções customizadas em `Problem`. + +## 3. Exemplos de uso: Problems + +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 `Property` para 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 `typeId` e descrição via `ProblemDetailsOptions`. + +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; configure `ExceptionOptions` e `ExceptionHandler` se necessário. + +Exemplos por categoria: + +```csharp +// 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"); +``` + +Extensões e propriedades encadeadas: + +```csharp +p400.With("attempt", 1).ChainProperty("User", 0); // User[0].name +p422.With("policy", "minimum-total"); +``` + +### Validando classes de forma padronizada + +Para validar as propriedades de uma classe pode ser criado um método HasProblems que retorna os problemas encontrados: + +```csharp +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. + + +### Custom e RFC 9457 (type) + +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 `type` estável, único e documentado, de preferência uma URI absoluta (ex.: `https://api.seu-dominio.com/problems/order-on-hold`). +- Inclua `title` humano-legível e `status` coerente 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:blank` para problemas customizados; descreva tipos próprios com documentação. + +Impacto do `Problems.Custom(detail, typeId, property)` na conversão: +- O `typeId` é mapeado para `ProblemDetails.Type`. +- Se configurado em `ProblemDetailsOptions.Descriptor`, define `title` e `status` consistentes para o tipo. +- Sem descrição, será usado um tipo agregado ou padrão conforme contexto. + +Exemplo de configuração do tipo no `ProblemDetailsOptions`: +```csharp +var options = new ProblemDetailsOptions(); +options.Descriptor.Add(new ProblemDetailsDescription( + typeId: "order-on-hold", + title: "Order on hold", + description: "Business rule violation", + statusCode: System.Net.HttpStatusCode.Conflict)); + +Problems problem = Problems.Custom("Order is on hold due to risk analysis", "order-on-hold"); +var pd = problem.ToProblemDetails(options); // Type será "tag:problemdetails/.problems#order-on-hold" ou uma URI absoluta se configurada +``` + +Converter coleção de problemas para exceção: +```csharp +var ex = (p400 + p422).ToException("Validation errors: {0}"); +throw ex; +``` + +## 4. Exemplos de uso: Result + +Construção e verificação: +```csharp +Result ok = "Hello"; +Result 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: +```csharp +var res = ok.Map(v => v.Length); // Result +var next = ok.Continue(v => Result.Ok()); // Result + +var async = await ok.MapAsync(v => Task.FromResult(v.Length)); + +// 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())); +``` + +Casos de uso reais (serviços, handlers, repositórios): +```csharp +// Result sem valor +public readonly struct UserService +{ + private readonly IUserRepository _repo; + private readonly IUserValidator _validator; + private readonly IUserPolicy _policy; + + public Result Create(UserInput input) + { + // validação de entrada + if (input.HasProblems(out var problems)) + return problems; // 400. + + // regra de negócio + if (_validator.EnsureIsValid(input).HasProblems(out var problems)) + return problems; // 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 Task DisableAsync(int id) + { + var findUser = _repo.FindByIdAsync(id); + if (findUser.NotFound(out var problem)) + return Task.FromResult((Result)problem); // 404 + + findUser.Entity.Disable(); + + return Result.Ok(); + } +} + +// Result com valor +public readonly struct OrderService +{ + private readonly IOrderRepository _repo; + + public Result Get(int id) + { + var found = _repo.TryFind(id); // retorna FindResult + 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 para `ProblemDetails` em APIs. +- Facilita composição funcional (Map, Continue, Match), reduzindo boilerplate e melhorando legibilidade. +- Integra com validação (`FluentValidation`) e persistência (EF `FindResult`). + +Performance: `Result` é um `readonly struct` +- Structs evitam alocação de heap em cenários comuns e permitem passagem por valor eficiente. +- `readonly` garante imutabilidade e melhor otimização pelo JIT. +- Métodos marcados com `AggressiveInlining` reduzem overhead em chamadas frequentes. +- Em pipelines síncronos/assíncronos curtos, reduz GC pressure em comparação com exceções. + +Agregação de problemas: + +```csharp +Result r1 = Problems.InvalidParameter("A"); +Result r2 = Problems.InvalidParameter("B"); +r1 += r2; // combina problemas +``` + +## 5. Exemplos para Entidades (Id, FindResult, TryFindAsync) + +A extensão de Entity Framework fornece métodos `TryFindAsync` e `TryFindByAsync` que retornam um `FindResult`. +Esse tipo encapsula o resultado da busca: a entidade encontrada (`Entity`) ou um problema padronizado quando não encontrada. + +- `TryFindAsync(DbContext, Id)` e `TryFindAsync(DbSet, Id)`: + - Quando a entidade não existe, gera um `Problem` com categoria `NotFound` (HTTP 404) e mensagem bem definida. + - Campos extras adicionados em `Extensions`: + - `id`: o valor do identificador usado na busca. + - `entity`: o nome da entidade (ex.: `TestEntity`). + +- `TryFindByAsync(DbContext/DbSet, Expression>)` e sobrecargas com nomes: + - Quando o filtro não encontra a entidade, gera `Problem` `NotFound` com detalhe incluindo o display name da entidade e o nome/valor do campo. + - Campos extras em `Extensions`: + - `` ou alias informado (ex.: `Name` ou `name`): valor usado no filtro. + - `entity`: nome da entidade. + +Uso típico: +```csharp +// Buscar por Id +Id id = 4; +var entry = await db.TestEntities.TryFindAsync(id); +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; // como Result/Problems ou ProblemDetails +} + +// Buscar por propriedade +var byName = await db.TestEntities.TryFindByAsync(e => e.Name == "Test4"); +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; +} +``` + +Quando possui nomes customizados (ex.: título do campo, alias e valor), use a sobrecarga: +```csharp +var entry2 = await db.TryFindByAsync(e => e.Name == "Test4", displayName: "Name", alias: "name", value: "Test4"); +if (entry2.NotFound(out var p)) +{ + // p.Extensions: { name: "Test4", entity: "TestEntity" } +} +``` + +Composição com `FindResult`: +```csharp +await entry.ContinueAsync(async entity => +{ + // prossiga com a entidade + return Result.Ok(); +}); +``` + +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: +```csharp +var res = entry.ToResult("id"); +// Se o parâmetro "id" for inválido, retorna Problem InvalidParameter com detail e property padronizados. +``` + +## 6. Resultados de API (OkMatch, NoContentMatch, CreatedMatch) + +Os tipos `OkMatch`, `NoContentMatch` e `CreatedMatch` permitem mapear `Result`/`Result` para respostas HTTP padronizadas, convertendo automaticamente problemas em `ProblemDetails` (RFC 9457) quando necessário. + +Exemplos baseados em `MatchApi`: +```csharp +// POST: cria e retorna 201 com Location e corpo +private static async Task> 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> 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 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 UpdatePersonAge(int id, PersonUpdateAge model) +{ + await Task.Delay(10); + return _personService.UpdatePersonAge(id, model); +} + +// DELETE: retorna 204 ou 404 ProblemDetails +private static async Task 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 `ProblemDetails` com status coerente (404, 400, etc.). + +Boas práticas (RFC 9457): +- Para `CreatedMatch`, forneça `Location` com URI absoluta ou relativa estável. +- Títulos (`title`) claros e condizentes com o `type`; descrição (`detail`) objetiva. +- Use `instance` quando aplicável para identificar o recurso/ocorrência. + +## 7. Cliente HTTP: ToResultAsync + +Métodos `HttpResultExtensions.ToResultAsync` desserializam respostas HTTP em `Result`/`Result`: +- Em sucesso (2xx): retornam `Result.Ok()` ou `Result` com o corpo JSON. +- Em falha (4xx/5xx): lê `application/problem+json` e converte para `Problems`; se não for ProblemDetails, tenta texto puro ou leitor customizado. + +Assinaturas principais: +```csharp +Task ToResultAsync(this HttpResponseMessage response, CancellationToken token = default); +Task> ToResultAsync(this HttpResponseMessage response, JsonSerializerOptions? options = null, CancellationToken token = default); +Task> ToResultAsync(this HttpResponseMessage response, JsonTypeInfo jsonTypeInfo, CancellationToken ct = default); +// Com FailureTypeReader para conteúdo de erro não-ProblemDetails +Task> ToResultAsync(this HttpResponseMessage response, FailureTypeReader? reader, JsonSerializerOptions? options = null, CancellationToken token = default); +Task> ToResultAsync(this HttpResponseMessage response, FailureTypeReader? reader, JsonTypeInfo jsonTypeInfo, CancellationToken ct = default); +``` + +Exemplos reais de consumo com `HttpClient`: + +```csharp + +var http = new HttpClient { BaseAddress = new Uri("https://api.exemplo.com") }; + +// 1) GET com corpo: sucesso → Result, falha → Problems +var respGet = await http.GetAsync("/users/123"); +var userResult = await respGet.ToResultAsync(); +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+json` para falhas; clientes devem interpretar `type`, `title`, `status`, `detail`, `instance`. +- Use `type`/`instance` URIs estáveis; evite conflitar extensões com campos reservados. + +## 8. Boas Práticas + +- 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.Custom` e descreva em `ProblemDetailsOptions`. +- Siga o RFC 9457: + - Prefira URIs absolutas para `type` e `instance`, títulos claros (`title`) e `status` coerente. + - Não sobrescreva campos reservados; use `Extensions` com nomes estáveis e significativos. +- Use `Result`/`Result` como fluxo de sucesso/falha: + - Componha com `Map`, `Continue`, `Match`/`MatchAsync` para reduzir boilerplate. + - Evite exceções para casos esperados; retorne problemas nas falhas. +- Valide entrada e regras de domínio: + - Modelo com `HasProblems(out Problems?)` ou FluentValidation (`EnsureIsValid`, `ToProblems`). + - Em APIs, converta problemas para `ProblemDetails` automaticamente via `OkMatch`/`CreatedMatch`/`NoContentMatch`. +- Persistência e buscas: + - Use `TryFindAsync`/`TryFindByAsync` (EF) e trate `FindResult` com `NotFound`/`ToResult([param])`. + - Propague campos extras (`id`, `entity`, `property/value`) em `Extensions` para rastreabilidade. +- Cliente HTTP: + - Consuma com `ToResultAsync` (valor ou problemas) e trate `application/problem+json` corretamente. + - Para erros não-ProblemDetails, considere `FailureTypeReader`. +- 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. +- Performance: + - `Result` é `readonly struct`; aproveite composição leve e evite alocações desnecessárias. + +## Resumo + +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` 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` e `NoContentMatch` simplificam respostas HTTP consistentes. +O uso de `readonly struct` e APIs inlinadas favorece performance, e as extensões (`With`, `ChainProperty`) melhoram rastreabilidade. + +## Instruções para Ferramentas de IA (GitHub Copilot) + +Diretrizes de geração alinhadas às seções 1–7: +- 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 `Property` para apontar o campo e `With(key, value)` para contexto adicional; `ChainProperty(parent[, index])` para caminhos. +- Resultados e composição + - Retorne `Result`/`Result` em serviços/handlers; componha com `Map`, `Continue`, `Match` e `MatchAsync`. + - Evite exceções para fluxos esperados; converta para problemas e propague via `Result`. +- APIs Web (servidor) + - Converta `Result`/`Result` em `OkMatch`, `CreatedMatch` (com `Location`) e `NoContentMatch`. + - Configure `ProblemDetailsOptions` e descreva `typeId` para problemas customizados; respeite RFC 9457 (`type`, `title`, `status`, `detail`, `instance`). +- Entity Framework + - Use `TryFindAsync`/`TryFindByAsync` para obter `FindResult`; converta para `Result` com `ToResult([param])`. + - Ao não encontrar, retorne `NotFound` padronizado com `Extensions` (`id`, `entity`, `property/value`). +- Cliente HTTP + - Consuma com `ToResultAsync` (valor ou problemas); trate `application/problem+json` e use `FailureTypeReader` para conteúdos não-ProblemDetails. +- Validação + - Implemente `HasProblems(out Problems?)` ou use FluentValidation (`EnsureIsValid`, `ToProblems`) para criar `Problems`. +- Performance e observabilidade + - Prefira `Result` (`readonly struct`) para menor alocação; use `With`/`Extensions` para dados de diagnóstico. + +Padrões de prompt para Copilot: +- “Implemente um serviço que valide entrada com FluentValidation, retorne `Result` e mapeie para `CreatedMatch` com Location.” +- “Crie uma consulta EF com `TryFindByAsync` por `Name`; retorne `OkMatch` quando encontrado e `ProblemDetails 404` quando não.” +- “Compose um `Result` em `Result` usando `Map` e trate falhas com `Match` → `Problems.AsResult()`.” +- “Defina um `Problems.Custom` com `typeId` e configure `ProblemDetailsOptions` seguindo RFC 9457 (URI absoluta em `type`).” +- “Consuma um endpoint com `HttpClient` e `ToResultAsync`; em falha, itere `Problems` e exiba `category`/`detail`.” \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/.docs/search.md b/RoyalCode.EnterprisePatterns/.docs/search.md new file mode 100644 index 0000000..e5f2fb7 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/.docs/search.md @@ -0,0 +1,278 @@ +# Documentação da API SmartSearch (Filters, Specifiers, Selectors, Sorting, Criteria) + +Esta documentação apresenta os conceitos, funcionalidades e exemplos práticos para usar a família de bibliotecas SmartSearch em projetos .NET +(alvo: .NET 8, .NET 9 e .NET 10). Também serve como referência para ferramentas de IA (ex.: GitHub Copilot) +para gerar código correto com base na API e nos padrões desta solução. + +Sumário +1. Introdução +2. Pacotes e responsabilidades +3. Conceitos centrais +4. Como o filtro é resolvido em expressão +5. Disjunção (OR) por grupo e por nome/caminho +6. Sorting e paginação com ResultList +7. Projeção para DTO (Selector) +8. Orquestração com Criteria e EF Core +9. Exemplos de uso +10. Pontos de extensão e configuração +11. Boas práticas +12. Resumo +13. Instruções para Ferramentas de IA (GitHub Copilot) + +## 1. Introdução + +SmartSearch implementa o padrão Specification/Filter-Specifier para compor filtros declarativos em `IQueryable`, gerar expressões LINQ traduzíveis pelo EF Core, aplicar ordenação e paginação, e projetar entidades em DTOs através de `Selector`. O objetivo é criar componentes de busca reutilizáveis, desacoplados, testáveis e extensíveis, evitando lógica manual e repetitiva. + +Benefícios principais: +- Filtros declarativos via atributos e convenções, com ignorância de valores vazios. +- Composição automática de predicados (Equal, Like, In, Range etc.). +- OR-disjunction por grupos e por nomes/caminhos contendo `Or`. +- Ordenação dinâmica por nomes de propriedades (inclusive caminhos aninhados). +- Projeção expressiva para DTO (nested, coleções, enums, nullables). +- Integração com EF Core via `ISearchManager` e `ICriteria`. +- Extensões para fábricas de predicados e geradores de expressão customizados. + +## 2. Pacotes e responsabilidades + +- `RoyalCode.SmartSearch.Core`: Abstrações para filtro, specifier, resultado (inclui paginação), sorting e seleção. +- `RoyalCode.SmartSearch.Linq`: Resolução de propriedades, geração de expressões, `Selector` e sorting dinâmico. +- `RoyalCode.SmartSearch.EntityFramework`: Integração EF Core, DI e `ISearchManager`/`ICriteria`. +- `RoyalCode.SmartSearch.Abstractions`: Contratos e interfaces compartilhadas. + +## 3. Conceitos centrais + +- `Filter`: objeto com propriedades que representam critérios de busca. Atributos nas propriedades controlam operador, negação, caminho de destino (`TargetPropertyPath`), e regras de ignorar valores vazios. +- `Specifier`: aplica o filtro em um `IQueryable` construindo a expressão-predicado. +- `Selector`: mapeia entidades para DTOs via expressão gerada automaticamente. +- `Sorting`: define ordenação dinâmica por propriedades, usado também na construção de `ResultList` com metadados. +- `Criteria`: orquestra filtro, ordenação, paginação, projeção e coleta dos resultados. + +Principais atributos e opções (exemplos): +- `Criterion`: define o alvo e operador; `TargetPropertyPath` pode apontar caminho aninhado. +- `Disjuction("alias")`: agrupa propriedades em OR. +- `ComplexFilter`: elege um membro para tratamento de filtro complexo (nested/owned types). +- `FilterExpressionGenerator`: delega a criação da expressão para gerador customizado. +- `DisableOrFromName` (em `Criterion`): desativa a inferência automática de OR quando o nome da propriedade (ou caminho) contém o token `Or`; trata o membro como critério único. + +## 4. Como o filtro é resolvido em expressão + +A pipeline de resolução constrói uma lista de resoluções (`ICriterionResolution`) por propriedade do filtro e compõe uma única expressão final. + +Etapas (alto nível): +1) Fábricas de predicado configuradas: propriedades mapeadas manualmente geram resoluções dedicadas. +2) Disjunção por `[Disjuction]`: membros do mesmo grupo se tornam uma única resolução com OR. +3) `Or` em nome/caminho: propriedades ou paths com `Or` são divididos em partes e resolvidos como OR. +4) ComplexFilter: propriedades com `[ComplexFilter]` (no tipo ou no membro) são tratadas como filtro composto. +5) Padrão: `DefaultOperatorCriterionResolution` escolhe o operador conforme o tipo (strings → Like; coleções → In; numéricos/datas → Equal), com guardas para ignorar valores vazios. + +Ignorar valores vazios ("IgnoreIfIsEmpty") cobre: `string` em branco, `Nullable` não definido, coleções vazias, structs default. + +## 5. Disjunção (OR) por grupo e por nome/caminho + +- `[Disjuction("g1")]`: quando múltiplas propriedades compartilham o mesmo alias, aplicam OR entre elas; entradas vazias são ignoradas. +- `Or` em propriedade/caminho: nomes como `FirstNameOrMiddleNameOrLastName` ou paths como `FirstNameOrLastName` dividem em vários destinos, combinados com OR. + - Opt-out por propriedade: use `[Criterion(DisableOrFromName = true)]` quando o token `Or` fizer parte do nome mas não indicar disjunção (ex.: `ColorOrSizePreference`). + +Casos esperados: +- Todos vazios → nenhum `Where` aplicado. +- Um valor presente → condição única. +- Múltiplos valores → OR entre as condições. + +## 6. Sorting e paginação com ResultList + +`Sorting` permite ordenar por nome de propriedade e direção. A paginação é modelada em `ResultList` com campos: `Page`, `ItemsPerPage`, `Count`, `Pages`, `Sortings`, e `Items`. + +Compatibilidade: ordenações podem ser fornecidas via JSON/string e são serializáveis. + +## 7. Projeção para DTO (Selector) + +`DefaultSelectorExpressionGenerator.Generate()` mapeia: +- Propriedades de mesmo nome. +- Caminhos aninhados (ex.: `Complex.Value` → `ComplexValue`). +- Normalização de `Nullable` para tipos não-nulos em DTO. +- Enum ↔ enum com conversão de valor. +- Sub-selects aninhados e coleções (element mapping), inclusive multi-nível. + +## 8. Orquestração com Criteria e EF Core + +Registre as entidades e configurações no DI e use `ISearchManager` para obter `ICriteria`: + +```csharp +services.AddEntityFrameworkSearches(cfg => +{ + cfg.Add(); + cfg.AddOrderBy("Name", x => x.Name.First); + cfg.AddSelector(x => new MyDto { Id = x.Id, Name = x.Name.First }); +}); + +var manager = provider.GetRequiredService>(); +var criteria = manager.Criteria(); +``` + +`ICriteria` fornece: +- `FilterBy(filter)` +- `OrderBy(ISorting)`/`OrderBy(IEnumerable)` +- `Select()` +- `Collect()`/`CollectAsync()` e modo busca `AsSearch().ToList()/ToListAsync()` +- `Exists()`, `FirstOrDefault()`, `Single()` com validação de cardinalidade + +Diferenças entre `Collect/CollectAsync` e `AsSearch().ToList/ToListAsync`: +- `Collect/CollectAsync`: retorna a coleção de itens já filtrados/ordenados/projetados, sem metadados de paginação. Em EF Core, mantém as entidades anexadas ao `ChangeTracker` (rastreadas), permitindo atualizações subsequentes e detecção de mudanças. +- `AsSearch().ToList/ToListAsync`: retorna `ResultList` com metadados de busca (Page, ItemsPerPage, Count, Pages, Sortings) aplicando defaults configurados. Use quando precisa de paginação, ordenação serializável e informações agregadas para UI/APIs. + +Quando usar: +- Use `Collect`/`CollectAsync` para rotinas internas, processamento batch, cenários em que você pretende modificar entidades (EF Core tracking) ou precisa manter o estado rastreado após a consulta. Evite em endpoints públicos se não precisar de tracking para reduzir overhead. +- Use `AsSearch().ToList/ToListAsync` em APIs/UI que exibem páginas e ordenações, quando não precisa manter entidades rastreadas pelo EF Core, priorizando resultados materializados com metadados e menor custo de tracking. + +## 9. Exemplos de uso + +### 9.1. Busca simples com filtro +```csharp +public sealed class SimpleModel { public int Id { get; set; } public string Name { get; set; } = string.Empty; } +public sealed class SimpleFilter { [Criterion] public string? Name { get; set; } } + +var criteria = provider.GetRequiredService>(); +criteria.FilterBy(new SimpleFilter { Name = "B" }); +var results = criteria.Collect(); // retorna apenas registros com Name semelhante a "B" (Like) +``` + +### 9.2. OR por nome de propriedade +```csharp +public sealed class PersonFilter +{ + [Criterion] // string → operador Like + public string? FirstNameOrMiddleNameOrLastName { get; set; } +} + +var res = criteria.FilterBy(new PersonFilter { FirstNameOrMiddleNameOrLastName = "Ann" }).Collect(); +// Aplica OR entre os paths FirstName, MiddleName e LastName ignorando campos vazios +``` + +### 9.3. OR por TargetPropertyPath +```csharp +public sealed class QueryFilter +{ + [Criterion(TargetPropertyPath = "FirstNameOrLastName")] public string? Query { get; set; } +} + +var res = criteria.FilterBy(new QueryFilter { Query = "Jo" }).Collect(); +// Aplica OR entre FirstName e LastName +``` + +### 9.4. Disjunção por grupo +```csharp +public sealed class DisjunctionFilter +{ + [Disjuction("g1")] public string? Email { get; set; } + [Disjuction("g1")] public string? Phone { get; set; } +} + +var res = criteria.FilterBy(new DisjunctionFilter { Email = "@domain" }).Collect(); +// Apenas Email gerará condição; Phone vazio é ignorado; múltiplos valores criam OR +``` + +### 9.5. ComplexFilter (tipos complexos/owned) +```csharp +public readonly record struct Email(string Value); +public sealed class User { public Email? Email { get; set; } } + +public sealed class UserFilter +{ + [Criterion("Email.Value")] public string? Email { get; set; } // mapeia para caminho aninhado +} + +criteria.FilterBy(new UserFilter { Email = "@royalcode" }); +var list = criteria.Collect(); +``` + +### 9.6. Sorting e paginação +```csharp +criteria.OrderBy(new Sorting { OrderBy = "Name", Direction = ListSortDirection.Ascending }); +var page1 = criteria.AsSearch().ToList(); +// page1 contém metadados (Page, ItemsPerPage, Count, Pages, Sortings) +``` + +### 9.7. Projeção para DTO +```csharp +public sealed class PersonDto { public int Id { get; set; } public string Name { get; set; } = string.Empty; } + +criteria.Select(); +var dtos = criteria.AsSearch().ToList(); +// Projeta conforme configurado (por nome ou configurador AddSelector) +``` + +### 9.8. Cardinalidade e existência +```csharp +criteria.FilterBy(new SimpleFilter { Name = "A" }); +var exists = criteria.Exists(); // true/false +var first = criteria.FirstOrDefault(); +var single = criteria.Single(); // lança se houver 0 ou >1; use filtros/ordenadores adequados +``` + +## 10. Pontos de extensão e configuração + +Via `ISearchConfigurations`: +- `Add()`: registra entidade para buscas. +- `AddOrderBy(name, keySelector)`: registra ordenação por nome. +- `AddSelector(expr)`: define seleção para DTO. +- `ConfigureSpecifierGenerator(opt => opt.For(f => f.Prop).Predicate(val => e => ...))`: mapeia fábrica de predicado customizada. + +`FilterExpressionGenerator`: implemente `ISpecifierExpressionGenerator` para cenários complexos (ex.: períodos): +```csharp +public sealed class PeriodSpecifierExpressionGenerator : ISpecifierExpressionGenerator +{ + public static Expression GenerateExpression(ExpressionGeneratorContext ctx) + { + // constrói a expressão Where com range calculado + // retorno é uma atribuição no `ctx.Query` com Queryable.Where(...) + // ver exemplos em README + throw new NotImplementedException(); + } +} +``` + +## 11. Boas práticas + +- Prefira filtros declarativos com atributos e caminhos aninhados claros. +- Use OR por grupos (`Disjuction`) ou por `Or` em nomes/caminhos quando fizer sentido de negócio. +- Configure ordenações nomeadas via `AddOrderBy` para evitar strings mágicas espalhadas. +- Projeção por `Selector` mantém consultas traduzíveis pelo EF Core; evite lógica não traduzível. +- Ignore vazios para não poluir o `Where` com condições inúteis; valide entrada com SmartValidations. +- Em APIs, componha com SmartProblems: converta falhas para `ProblemDetails` (RFC 9457). + +## 12. Resumo + +SmartSearch fornece uma forma padronizada, declarativa e performática de compor buscas em `IQueryable` e EF Core. Com `Filter` + `Specifier`, você gera predicados automaticamente; com `Sorting` e `ResultList`, você controla ordenação e paginação; com `Selector`, projeta DTOs complexos de forma segura. A integração via `ICriteria` orquestra todas as etapas (filtro, ordenação, seleção e coleta), e pontos de extensão permitem adaptar o comportamento a regras avançadas. + +## 13. Instruções para Ferramentas de IA (GitHub Copilot) + +Objetivo: gerar buscas seguindo o contrato da API (Filter + Specifier + Criteria) e produzir consultas EF Core traduzíveis. + +Princípios obrigatórios +- Modele filtros como classes com propriedades e anote com `Criterion`/`Disjuction`/`ComplexFilter` quando necessário. +- Use `ICriteria` para aplicar `FilterBy`, `OrderBy`, `Select` e `Collect`/`AsSearch`. +- Evite construir `Expression` manualmente quando um atributo cobre o caso; use `FilterExpressionGenerator` apenas para cenários avançados. +- Preserve nomes/caminhos de propriedades usando `TargetPropertyPath` e convenções com `Or` para OR. + +Padrões de implementação +- Filtro simples: `class Filter { [Criterion] public string? Name { get; set; } }` e `criteria.FilterBy(filter)`. +- Disjunção: `[Disjuction("g1")]` em múltiplos membros do filtro para OR. +- OR por caminho: `[Criterion(TargetPropertyPath = "FirstNameOrLastName")]`. +- Projeção: `criteria.Select()` usando mapeamento por nome ou configurado. +- Ordenação: `criteria.OrderBy(new Sorting { OrderBy = "Name" })`. +- Coleta vs Busca paginada: para lista simples, prefira `Collect/CollectAsync`; para UI/APIs com paginação e metadados, use `AsSearch().ToList/ToListAsync` e leia `ResultList`. +- EF Core Tracking: `Collect` mantém entidades rastreadas no `ChangeTracker`; `AsSearch().ToList` não anexa entidades. Escolha conforme necessidade de atualização subsequente vs desempenho. + +Integrações +- EF Core: obtenha `ICriteria` via `ISearchManager` e registre com `AddEntityFrameworkSearches(cfg => cfg.Add())`. +- SmartProblems/SmartValidations: valide entrada e converta falhas para `ProblemDetails` antes da busca. + +Antipadrões (evitar) +- Criar `Where` manual com strings de propriedade sem usar atributos/convenções da lib. +- Usar expressões não traduzíveis pelo EF em `Selector`. +- Ignorar valores vazios e forçar filtros que resultam em consultas ineficientes. + +Exemplos de prompts corretos +- "Implemente um filtro com `[Disjuction("g1")]` para Email/Phone e aplique com `ICriteria.FilterBy(filter)` retornando `ResultList`." +- "Crie um `Selector` para `Order -> OrderDto` e projete com `criteria.Select().AsSearch().ToList()`." +- "Configure `AddOrderBy(\"CustomerName\", x => x.Customer.Name)` e gere `criteria.OrderBy(new Sorting { OrderBy = \"CustomerName\" })`." diff --git a/RoyalCode.EnterprisePatterns/.docs/selector.md b/RoyalCode.EnterprisePatterns/.docs/selector.md new file mode 100644 index 0000000..c62b5bd --- /dev/null +++ b/RoyalCode.EnterprisePatterns/.docs/selector.md @@ -0,0 +1,240 @@ +# Documentação da API SmartSelector (Selectors, AutoSelect, AutoProperties) + +Esta documentação apresenta os conceitos, funcionalidades e exemplos práticos para usar a família de bibliotecas SmartSelector em projetos .NET (alvo: .NET 8, .NET 9 e .NET 10 runtime; Generator .NET Standard 2.0 como Analyzer). Também serve como referência para ferramentas de IA (ex.: GitHub Copilot) gerarem código correto com base na API e nos padrões desta solução. SmartSelector integra-se naturalmente com SmartSearch para compor consultas EF Core com projeções tipadas. + +Sumário +1. Introdução +2. Pacotes e responsabilidades +3. Conceitos centrais +4. Atributos disponíveis e regras +5. Membros gerados pelo Source Generator +6. Propriedades suportadas em `AutoProperties` +7. Flattening (caminhos aninhados por convenção) +8. MapFrom (alias explícito de origem) +9. Extensões de `IQueryable`/`IEnumerable` +10. Integração com SmartSearch +11. Exemplos de uso +12. Boas práticas +13. Limitações e diagnósticos +14. Instruções para Ferramentas de IA (GitHub Copilot) + +## 1. Introdução + +SmartSelector é um Roslyn Source Generator que cria automaticamente projeções fortemente tipadas para DTOs, reduzindo boilerplate em consultas LINQ/EF Core. A partir de classes parciais anotadas com atributos, o gerador produz: +- `Expression>` reutilizáveis e traduzíveis pelo EF Core. +- Métodos de fábrica `From(TFrom)` com cache de delegate. +- Métodos de extensão para `IQueryable` e `IEnumerable`. +- Propriedades simples em DTOs via `AutoProperties`. +- Flattening por convenção (nomes concatenados em PascalCase → caminhos aninhados). + +Foco: mapeamento 1x1 estilo Adapt (Mapster), sem resolvers customizados nem naming policies. + +## 2. Pacotes e responsabilidades + +- `RoyalCode.SmartSelector` (runtime): + - Tipos base e atributos: `AutoSelectAttribute`, `AutoPropertiesAttribute`, `MapFromAttribute`. + - Extensões de seleção para `IQueryable`/`IEnumerable` geradas por modelo. + +- `RoyalCode.SmartSelector.Generators` (analyzer .NET Standard 2.0): + - Gera código parcial (*.g.cs) a partir dos atributos. + - Emite diagnósticos quando o uso é incorreto. + +## 3. Conceitos centrais + +- `Model`/`Details`: DTO alvo da projeção (classe parcial). +- `TFrom`: tipo origem da projeção (entidade EF, record, classe). +- `AutoSelect`: habilita geração de expressão e extensões. +- `AutoProperties` / `AutoProperties`: gera propriedades simples automaticamente. +- Flattening: casamento por convenção de PascalCase para caminhos aninhados. +- `MapFrom`: alias explícito de origem para propriedades do DTO. + +## 4. Atributos disponíveis e regras + +### 4.1 `AutoSelect` +- Declara o tipo de origem e ativa geração de: + - `Select{TFrom}Expression` em `Model`. + - Métodos de extensão `Select{Model}` para `IQueryable`/`IEnumerable`. +- Regras: + - A classe alvo deve ser `partial`. + - Opcionalmente combine com `AutoProperties`. + +### 4.2 `AutoProperties` e `AutoProperties` +- Gera propriedades simples no DTO com base em `TFrom`. +- Quando usado sem genérico, o `TFrom` é inferido do `AutoSelect`. +- Suporta exclusão por `Exclude = [ nameof(T.Prop) ]`. + +### 4.3 `MapFromAttribute` +- Permite mapear explicitamente uma propriedade do DTO a um membro de `TFrom` por nome. +- Assinatura: + - `new MapFromAttribute(string propertyName)`. +- Preferir `nameof(TFrom.Member)` para segurança em refactors. + +## 5. Membros gerados pelo Source Generator + +Em um `Model` anotado: +- Campo de cache: `private static Func select{TFrom}Func;` +- Expressão: `public static Expression> Select{TFrom}Expression => a => new Model { ... }`. +- Fábrica: `public static Model From(TFrom a) => (selectFunc ??= SelectExpression.Compile())(a);` +- Extensões (classe estática): + - `IQueryable SelectModel(this IQueryable query)`. + - `IEnumerable SelectModel(this IEnumerable enumerable)`. + - `Model ToModel(this TFrom source)`. + +Todos visam manter consultas traduzíveis pelo EF Core. + +## 6. Propriedades suportadas em `AutoProperties` + +- Primitivos numéricos, `bool`, `string`, `char`. +- `DateTime` e nullables simples. +- `enum`, `struct`. +- `IEnumerable` onde `T` é dos tipos acima/enum/struct. +- Exclusões por `Exclude` para membros indesejados. + +## 7. Flattening (caminhos aninhados por convenção) + +- Nomes de propriedades do DTO em PascalCase mapeiam cadeias da origem: + - `CustomerAddressCity` → `a.Customer.Address.City`. + - `CustomerAddressCountryRegionName` → `a.Customer.Address.Country.Region.Name`. +- Regras: + - Segmentos devem corresponder a propriedades navegáveis. + - Último segmento é o valor terminal (simples/suportado). + - Evite colisões de prefixo; declare DTOs aninhados ou use `MapFrom`. + +## 8. MapFrom (alias explícito de origem) + +- Use quando o nome do DTO não segue convenção de flattening ou há ambiguidade. +- Exemplo: + - `CustomName` mapeado de `nameof(Product.Name)`. +- Continua traduzível pelo EF Core por estar na `Expression` gerada. + +## 9. Extensões de `IQueryable`/`IEnumerable` + +- `Select{Model}(this IQueryable)` aplica a `Expression` gerada. +- `Select{Model}(this IEnumerable)` usa `From`. +- `To{Model}(this TFrom)` cria instância única. + +## 10. Integração com SmartSearch + +- Combine `SmartSelector` com `SmartSearch` para compor `Criteria` e projeção: + - `criteria.Select()` (SmartSearch) pode utilizar mapeamento por nome/caminho. + - Alternativamente, use os métodos `Select{Model}` gerados pelo SmartSelector. +- Diretrizes: + - Projeções devem ser traduzíveis (evite lógica client-side não suportada). + - Ordenação/paginação em SmartSearch funciona sobre `IQueryable` antes da projeção. + +## 11. Exemplos de uso + +### 11.1 Projeção simples + propriedades auto +```csharp +[AutoSelect, AutoProperties] +public partial class UserDetails { } + +// Uso EF Core +var list = db.Users.SelectUserDetails().ToList(); +var dto = UserDetails.From(user); +var expr = UserDetails.SelectUserExpression; +``` + +### 11.2 Objeto aninhado + exclusão +```csharp +[AutoSelect, AutoProperties(Exclude = [ nameof(Book.Sku) ])] +public partial class BookDetails +{ + public ShelfDetails Shelf { get; set; } +} + +[AutoProperties] +public partial class ShelfDetails { } + +// Trecho gerado +// new BookDetails { +// Shelf = new ShelfDetails { Id = a.Shelf.Id, Location = a.Shelf.Location }, +// Price = a.Price, +// // Sku excluído +// } +``` + +### 11.3 Flattening profundo +```csharp +public class Order { public Customer Customer { get; set; } } + +[AutoSelect] +public partial class OrderDetails +{ + public string CustomerAddressCountryRegionName { get; set; } +} + +// Expressão: CustomerAddressCountryRegionName = a.Customer.Address.Country.Region.Name +``` + +### 11.4 Alias explícito com `MapFrom` +```csharp +[AutoSelect] +public partial class CustomProductDetails +{ + [MapFrom(nameof(Product.Id))] + public Guid CustomId { get; set; } + + [MapFrom(nameof(Product.Name))] + public string CustomName { get; set; } +} +``` + +### 11.5 Apenas propriedades automáticas +```csharp +[AutoProperties] +public partial class ProductSnapshot { } +``` + +## 12. Boas práticas + +- Prefira consumir a `Expression` gerada (`Select{TFrom}Expression`) para reutilização e composição LINQ. +- Use `nameof` em `Exclude` e `MapFrom` para segurança. +- Em DTOs com caminhos longos, avalie modelos aninhados por clareza. +- Integre com SmartSearch antes da projeção quando precisar de paginação/ordenadores. +- Evite adicionar lógica não traduzível na projeção. + +## 13. Limitações e diagnósticos + +- Sem resolvers customizados ou transformações de tipo. +- Sem naming policies/renome global; use `MapFrom` para alias. +- Colisões de prefixo em flattening exigem desambiguação manual. +- Principais diagnósticos (exemplos): + - Classe não `partial` ou uso inválido (`RCSS000`). + - Propriedade não encontrada (`RCSS001`). + - Tipos incompatíveis (`RCSS002`). + - Uso incorreto de atributos (`RCSS003–RCSS005`). + +## 14. Instruções para Ferramentas de IA (GitHub Copilot) + +Objetivo: gerar projeções seguindo o contrato da API (`AutoSelect`, `AutoProperties`, `MapFrom`) e produzir código traduzível pelo EF Core. + +Princípios obrigatórios +- Anote DTOs com `AutoSelect` e (opcional) `AutoProperties`. +- Use `MapFrom` quando o nome do DTO não seguir a convenção ou houver ambiguidade. +- Não crie resolvers customizados; foque em mapeamento 1x1. +- Utilize métodos gerados `Select{Model}`/`To{Model}` em consultas. + +Padrões de implementação +- DTO mínimo: + - `public partial class Dto { }` com `[AutoSelect, AutoProperties]`. +- Flattening: + - Propriedades `PascalCase` concatenadas para acessar caminhos aninhados. +- Exclusões: + - `Exclude = [ nameof(T.Prop) ]` em `AutoProperties`. +- Integração com SmartSearch: + - Aplique `criteria.FilterBy(...)`/`OrderBy(...)` sobre `IQueryable` e depois `Select{Model}`. + +Antipadrões (evitar) +- Inserir lógica não traduzível em expressões geradas. +- Criar mapeamentos com conversões complexas/formatters. +- Usar exceções como fluxo de composição; prefira expressão + métodos gerados. + +Exemplos de prompts corretos +- "Crie um DTO com `[AutoSelect, AutoProperties]` e projete com `db.Users.SelectUserDetails().ToList()`." +- "Declare `OrderDetails` com flattening `CustomerAddressCountryRegionName` e consuma `SelectOrderExpression`." +- "Mapeie `CustomProductDetails` usando `[MapFrom(nameof(Product.Name))]` e gere expressões traduzíveis." + +--- +Happy coding! + diff --git a/RoyalCode.EnterprisePatterns/.docs/validations.md b/RoyalCode.EnterprisePatterns/.docs/validations.md new file mode 100644 index 0000000..7416d48 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/.docs/validations.md @@ -0,0 +1,360 @@ +# Documentação da API SmartValidations (RuleSet, IValidable) + +Esta documentação apresenta os conceitos, funcionalidades e exemplos práticos para usar a biblioteca SmartValidations em projetos .NET. +Também serve de referência para ferramentas de IA (ex.: GitHub Copilot) gerarem código correto com base na API da biblioteca. + +Projetos alvo: .NET 8, .NET 9 e .NET 10. + +Sumário +1. Introdução +2. Funcionalidades Principais +3. Exemplos de uso base +4. Exemplos de uso com structs (IValidable) +5. Exemplos de uso aninhados (objetos e coleções) +6. Exemplos de uso avançados (condicionais, regras personalizadas, internacionalização) +7. Referência da API +8. Boas práticas +9. Resumo +10. Instruções para Ferramentas de IA (GitHub Copilot) + +## 1. Introdução + +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 `Problems` padronizados. +- Composição forte e tipada: uso de `INumber`, `CallerArgumentExpression` e 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. + +## 2. Funcionalidades Principais + +- `RuleSet` + - DSL fluente para aplicar regras. Cada falha gera um `Problem` via `Problems.InvalidParameter(...)` com metadados: + - `rule` (nome da regra), `current` (valor atual), `expected` (valor(es) esperado(s)), `pattern` (em regras de regex), `properties` e `values` (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étodo `HasProblems(out Problems?)`. + +- `IValidable` + - Contrato simples com `HasProblems(out Problems?)` para permitir validação de objetos e value objects (structs). + +- Predicados internos (`BuildInPredicates`) + - Conjunto abrangente de verificações: vazios, igualdade, comparações, faixas, tamanhos, padrões de string, e utilitários de data/tempo. + - Utilizados por `RuleSet` para implementar as regras fluentes. + +- Validações aninhadas + - `Nested(...)` e `NotNullNested(...)` para objetos, coleções e tipos que implementam `IValidable`. + - Encadeia `Property` com `ChainProperty` e índices (ex.: `Items[2].Quantity`). + +- Regras condicionais e customizadas + - `When(...)` e `Unless(...)` (várias sobrecargas) para aplicar grupos de regras sob condição ou de forma alternativa. + - `Must(...)` e `BothMust(...)` para regras personalizadas com formatadores de mensagem. + +- Utilidades + - `WithPropertyPrefix(...)` para normalizar caminhos removendo prefixos conhecidos. + - Regras de e-mail e URL baseadas em `EmailAddressAttribute` e `UrlAttribute`. + +## 3. Exemplos de uso base + +Validação simples em um DTO usando `RuleSet` e retornando `Problems` via `HasProblems`: + +```csharp +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() + .NotEmpty(Name) + .Min(Age, 18) + .HasProblems(out problems); + } +} +``` + +Usando como serviço/handler, retornando `Problems?` diretamente: + +```csharp +public static Problems? ValidateProduct(string sku, string? name, decimal price) +{ + return Rules.Set() + .NotEmpty(sku) + .NullOrLength(name, 3, 120) + .GreaterThanOrEqual(price, 0) + ; // conversão implícita para Problems? +} +``` + +Checando e serializando em API: + +```csharp +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 +} +``` + +## 4. Exemplos de uso com structs (IValidable) + +Implemente `IValidable` em value objects e use `RuleSet.Validate` para coleções: + +```csharp +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() + .GreaterThanOrEqual(Amount, 0) + .Length(Currency, 3, 3) + .HasProblems(out problems); + } +} + +var prices = new[] { new Money(-1, ""), new Money(10, "USD") }; +var set = Rules.Set().Validate((IEnumerable)prices); +if (set.HasProblems(out var problems)) +{ + // problems conterá caminhos com índices: [0], [1] +} +``` + +## 5. Exemplos de uso aninhados (objetos e coleções) + +Validando objetos opcionais com `NotNullNested` e objetos sempre presentes com `Nested`: + +```csharp +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
? PastAddresses { get; set; } + + public bool HasProblems(out Problems? problems) + { + return Rules.Set() + .NotEmpty(CustomerId) + .NotNullNested(Shipping, addr => Rules.Set
() + .NotEmpty(addr.Street) + .NotEmpty(addr.City) + .NotEmpty(addr.ZipCode)) + .Nested(PastAddresses, addr => Rules.Set
() + .NotEmpty(addr.Street) + .NotEmpty(addr.City) + .NotEmpty(addr.ZipCode)) + .HasProblems(out problems); + } +} +``` + +Usando `WithPropertyPrefix` para normalizar nomes ao compor validadores reutilizáveis: + +```csharp +Problems? ValidateAddress(Address a) + => Rules.Set
() + .WithPropertyPrefix(nameof(a)) + .NotEmpty(a.Street) + .NotEmpty(a.City) + .NotEmpty(a.ZipCode); + +var set2 = Rules.Set() + .Nested(order.ShippingAddress, a => ValidateAddress(a)); +``` + +No exemplo acima, se `Street` estiver vazio, o `Property` do `Problem` será `ShippingAddress.Street`. + +Caso não usasse `WithPropertyPrefix`, o `Property` seria `ShippingAddress.a.Street`, incluindo o nome do parâmetro como nome da propriedade. + +## 6. Exemplos de uso avançados + +- Regras condicionais (`When`/`Unless`): + +```csharp +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, 100)); // alternativo +``` + +- Regras personalizadas (`Must`/`BothMust`) com metadados de regra: + +```csharp +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 via `DisplayNames`. + - Configure seus display names (DataAnnotations ou provedor customizado) para mensagens amigáveis. + +## 7. Referência da API + +Tipos principais: +- `RuleSet` (fluent API de validação) +- `IValidable` (contrato para validação) +- `BuildInPredicates` (predicados usados pelas regras) + +Criação e inspeção +- `Rules.Set()` / `Rules.Set()` / `RuleSet.For()` +- `HasProblems(out Problems? problems)` +- Conversão implícita `RuleSet -> Problems?` +- `WithPropertyPrefix(string)` + +Nulos e vazios +- `NotNull(value)` +- `NotEmpty` para: `string`, `INumber`, `T? where T: struct, INumber`, arrays, `ICollection`, `IReadOnlyCollection`, `IEnumerable`, `DateTime(Offset)`, `DateOnly`, `Guid` +- `NullOrNotEmpty` para: `string`, `INumber`, `T? where T: struct, INumber` +- Duais: `BothNullOrNotEmpty(string?, string?)` + +Igualdade/Desigualdade +- `Equal` e `NotEqual` para `string` (com `StringComparison`) e tipos `IEquatable` (inclui versões `Nullable`) +- Duais: `BothEqual` e `BothNotEqual` para `string` e tipos `IEquatable` + +Strings e padrões +- `Matches` / `NotMatches` com `string pattern` ou `Regex` +- `StartsWith` / `EndsWith` / `Contains` / `NotContain` +- `OnlyLetters` / `OnlyDigits` / `OnlyLettersOrDigits` / `NoWhiteSpace` + +Numéricos e faixas +- `Min` / `Max` / `MinMax` (e variantes `NullOrMin`, `NullOrMax`, `NullOrMinMax`) +- Tamanho de string: `MinLength` / `MaxLength` / `Length` (e `NullOrMinLength`, `NullOrMaxLength`, `NullOrLength`) + +Comparações relativas +- `LessThan` / `LessThanOrEqual` / `GreaterThan` / `GreaterThanOrEqual` (para tipos `IComparable` e suas variantes `Nullable`) + +Datas e horários (via `BuildInPredicates`) +- `InPast` / `InFuture` / `Today` para `DateTime`, `DateTimeOffset`, `DateOnly` +- `After` / `Before` / `Between` para os mesmos tipos + +E-mail e URL +- `Email(string?)` e `Url(string?)` + +Customização +- `Must(value, predicate, messageFormatter[, ruleName])` +- `Must(value, param, predicate, messageFormatter[, ruleName])` +- `BothMust(...)` e `BothMust(...)` + +Validação aninhada +- Objetos: `Nested(value, Problems? validator)` / `Nested(value, Func)` / `Nested(value) where T: IValidable` +- Coleções: `Nested(IEnumerable, ...)` com indexação automática +- Garantindo não-nulo: `NotNullNested(...)` (mesmas variações) + +Structs com `IValidable` +- `Validate(value) where T: struct, IValidable` +- `Validate(IEnumerable) 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`, `values` preenchidos conforme a regra. + +## 8. Boas práticas + +- Modele validação por request/DTO em uma única função que retorna `Problems?`. +- Use `IValidable`/`ValidateFunc` para compor validações de agregados, objetos aninhados e value objects. +- Prefira regras não-explosivas: use `Result`/`Problems` em vez de exceções em controle de fluxo. +- Defina `StringComparison` explicitamente quando relevante. +- Use `WithPropertyPrefix` para normalizar caminhos ao reutilizar validadores. +- Padronize mensagens com templates localizáveis e display names consistentes. +- Garanta cobertura com regras `NullOr*` quando campos forem opcionais. +- Em coleções, valide cada item com `Nested`/`Validate` para obter caminhos com índice. + +## 9. Resumo + +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. + +## 10. Instruções para Ferramentas de IA (GitHub Copilot) + +Objetivo: gerar validações seguindo o contrato da API (`RuleSet`, `IValidable`) e produzir `Problems` com metadados corretos. + +Princípios obrigatórios +- Sempre valide com `Rules.Set()` (quando dentro do tipo) ou `Rules.Set()` (fora), e finalize com `HasProblems(out Problems?)` ou conversão implícita para `Problems?`. +- Use os métodos de regra do `RuleSet` (ex.: `NotEmpty`, `Min`, `Equal`, `Matches`, `Nested`, `Validate`) em vez de criar `Problem` manualmente. +- Preserve o `Property` via `CallerArgumentExpression`: passe o próprio argumento observado à regra, não strings de nome manual. +- Em objetos opcionais, primeiro `NotNullNested(...)`; em objetos obrigatórios, use `Nested(...)` diretamente. +- Em coleções, use `Nested(IEnumerable, ...)` ou `Validate(IEnumerable)` para indexar automaticamente (`Items[i].Prop`). + +Padrões de implementação +- DTO/entrada + - Estrutura: + - Método `bool HasProblems(out Problems? problems)`. + - `return Rules.Set() ... .HasProblems(out problems);` + - Campos opcionais: `NullOr*` (ex.: `NullOrLength`) ou `NotNullNested` antes de regras internas. + - Campos obrigatórios: `NotEmpty`/`Min`/`Max`/`Length` conforme tipo. + +- Objetos aninhados + - `Nested(child, c => Rules.Set()...)` para validar filhos obrigatórios. + - `NotNullNested(child, c => Rules.Set()...)` para filhos opcionais. + - Para reuso, normalize com `WithPropertyPrefix(prefix)` no validador reutilizável. + +- Value objects (struct) + - Implementar `IValidable` com `HasProblems(out Problems?)` interno ao struct. + - Em agregados: `Rules.Set().Validate(collectionOfStructs)` para coletar e indexar problemas. + +- Condicionais + - `When(cond, builder)` para aplicar regras sob condição. + - `Unless(cond, builder)` para aplicar regras quando a condição não vale. + - Alternativas: `Unless(conditionRules, alternativeRules)` (se ambos falham, agregue ambos). + +- Regras customizadas + - `Must(value, predicate, messageFormatter[, ruleName])` e `BothMust(...)` quando não houver regra pronta. + - O `messageFormatter` deve usar o display name (`prop`) e indicar claramente o requisito violado. + +- URLs/e-mails e datas + - Use `Email(value)` e `Url(value)` para formatos; prefira `IsHttpsUrl`/`IsAbsoluteUrl` (via predicados) dentro de `Must` quando necessário. + - Datas: utilize comparações (`LessThan/GreaterThanOrEqual`) e predicados de tempo (`InPast/InFuture`) conforme o caso. + +Formato esperado de saída +- Funções devem retornar `Problems?` ou `bool HasProblems(out Problems?)`. +- Não lançar exceções para fluxo de validação. +- Problemas conterão `rule/current/expected/pattern/properties/values` automaticamente via `RuleSet`. + +Exemplos de prompts corretos +- "Gere `HasProblems(out Problems?)` para `RegisterUser` com `NotEmpty(Name)`, `Min(Age,18)`, e `NotNullNested(Address, addr => ... )`." +- "Valide `Order.Items` com `Nested(items, item => Rules.Set().NotEmpty(item.ProductId).GreaterThan(item.Quantity,0))`." +- "Crie regra customizada com `Must(Password, predicate, formatter, ruleName:"password.policy")` e alternativa com `Unless(s => s.NotEmpty(PromoCode), s => s.Min(Total,100))`." +- "Implemente `IValidable` em `struct Price` e valide uma lista com `Rules.Set().Validate(prices)` retornando `Problems?`." + +Antipadrões (evitar) +- Construir `Problem` manualmente para validação de entrada (use `RuleSet`). +- Passar nomes de propriedade como string (quebra refactors e `CallerArgumentExpression`). +- Usar exceções para fluxo esperado de validação. +- Usar Expression para a propriedade (x => x.Prop) (não funciona). \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/.docs/workcontext.md b/RoyalCode.EnterprisePatterns/.docs/workcontext.md index d95c1a5..a27a29d 100644 --- a/RoyalCode.EnterprisePatterns/.docs/workcontext.md +++ b/RoyalCode.EnterprisePatterns/.docs/workcontext.md @@ -1,54 +1,93 @@ -# RoyalCode Enterprise Patterns – WorkContext, UnitOfWork e Repositories -A `RoyalCode.WorkContext` é a API recomendada para uso. Ela estende o padrão Unit of Work e agrega funcionalidades de Repositórios, Busca (SmartSearch), Commands e Queries, oferecendo uma experiência unificada. +# Documentação da API WorkContext, UnitOfWork e Repositories -Este documento descreve: -- `RoyalCode.WorkContext.Abstractions`: contratos e builder do Work Context. -- `RoyalCode.UnitOfWork.Abstractions`: contratos do padrão Unit of Work consumidos pelo WorkContext. -- `RoyalCode.Repositories.Abstractions`: contratos do padrão Repository consumidos pelo WorkContext. -- Implementações EF Core: `RoyalCode.WorkContext.EntityFramework`, `RoyalCode.WorkContext.PostgreSql`, `RoyalCode.WorkContext.Sqlite`, `RoyalCode.WorkContext.SqlServer`. +Esta documentação apresenta os conceitos, funcionalidades e exemplos práticos para usar as bibliotecas de persistência desta solução. +Serve também como referência para ferramentas de IA (ex.: GitHub Copilot) compreenderem e gerarem código correto com base na API. -Targets: .NET 8, .NET 9, .NET 10 (via `$(LibTargets)`). +Projetos alvo: .NET 8, .NET 9 e .NET 10. + +Escopo principal: +- `RoyalCode.Repositories.Abstractions` (contratos Repository) +- `RoyalCode.UnitOfWork.Abstractions` (contratos Unit of Work) +- `RoyalCode.WorkContext.Abstractions` (contratos WorkContext) + +Implementações baseadas em EF Core: +- `RoyalCode.Repositories.EntityFramework` +- `RoyalCode.UnitOfWork.EntityFramework` +- `RoyalCode.WorkContext.EntityFramework` + +Providers (utilitários): +- `RoyalCode.WorkContext.SqlServer` +- `RoyalCode.WorkContext.PostgreSql` +- `RoyalCode.WorkContext.Sqlite` --- -## WorkContext (Abstrações) +## 1. Introdução + +`RoyalCode.WorkContext` é a API recomendada para uso. Ela estende o padrão Unit of Work e agrega funcionalidades de Repositórios, Busca (SmartSearch), Commands e Queries, oferecendo uma experiência unificada, modular e compatível com EF Core. + +Benefícios principais: +- Contratos limpos e tipados para Repository/UoW/WorkContext. +- Builder fluente para configurar DbContext, repositórios, buscas, commands e queries. +- Integração direta com EF Core e provedores (Sqlite, SqlServer, PostgreSQL via Npgsql). +- Suporte a comandos/consultas com registro por assembly. + +--- + +## 2. Abstrações e Conceitos + +WorkContext: +- `IWorkContext`: API unificada que expõe operações de Unit of Work, acesso a repositórios, buscas, envio de comandos e execução de consultas. +- `IWorkContextBuilder`: builder fluente para configurar WorkContext, DbContext (EF Core), repositórios, buscas, commands e queries. + +UnitOfWork: +- `IUnitOfWork`: controle transacional e persistência, com `Save()`/`SaveAsync()`. +- Integra com repositórios e pipelines de comandos/consultas. + +Repositories: +- Contratos para CRUD e consulta por ID, orientados a entidades (`IRepository`). +- Suporte a ID fortemente tipado com integrações auxiliares. -Conceitos principais -- `IWorkContext`: contexto unificado que expõe operações de UoW, acesso a repositórios, buscas, envio de comandos e execução de consultas. -- `IWorkContextBuilder`: builder fluente para configurar o WorkContext, DbContext (EF Core), repositórios, buscas, commands e queries. -- Commands/Queries: padrões de request/response integrados ao WorkContext. +--- + +## 3. Configuração com Entity Framework -Principais extensões de configuração (EntityFramework) +Principais extensões de configuração: - `ConfigureDbContextPool(...)`: registra o `DbContext` e configura o provider EF Core. - `ConfigureModel(...)` e `ConfigureMappingsFromAssembly(...)`: aplica configurações de modelagem/mapeamentos. -- `EnsureDatabaseCreated()`: garante que o banco seja criado (apenas para cenários de teste/dev). +- `EnsureDatabaseCreated()`: garante que o banco seja criado (dev/test). - `SeedDatabase(...)`: semeia dados iniciais. - `ConfigureRepositories(...)` e `AddRepositories(...)`: registra repositórios para entidades. - `ConfigureSearches(...)`: registra pesquisas (SmartSearch). -- `ConfigureCommands(...)` e `AddCommandDispatcher()`: registra handlers de comandos e o dispatcher opcional. -- `ConfigureQueries(...)`: registra handlers de consultas síncronas e assíncronas. +- `ConfigureCommands(...)` e `AddCommandDispatcher()`: registra handlers de comandos. +- `ConfigureQueries(...)`: registra handlers de consultas. -Mapa de provedores EF Core (como Copilot deve sugerir) +Mapa de provedores EF Core: - Sqlite in‑memory: `UseSqlite(new SqliteConnection("DataSource=:memory:"))` + `conn.Open()` - Sqlite arquivo: `UseSqlite("Data Source=mydb.sqlite")` - PostgreSQL (Npgsql): `UseNpgsql("Host=...;Database=...;Username=...;Password=...")` - SQL Server: `UseSqlServer("Server=...;Database=...;Integrated Security=True")` -Referência rápida de API -- Builder: `AddWorkContext()` → `ConfigureDbContextPool(...)` → `ConfigureModel(...)` → `ConfigureRepositories(...)` → `ConfigureSearches(...)` → `ConfigureCommands(...)` → `ConfigureQueries(...)` -- Operações: `Repository()`, `Criteria()`, `SaveAsync()`, `SendAsync(command)`, `QueryAsync(request)` -- Registros por assembly: `AddRepositories(assembly)`, `ConfigureSearches(assembly)`, `ConfigureCommands(assembly)`, `ConfigureQueries(assembly)` +--- + +## 4. Operações principais em WorkContext -Operações principais - `Repository()`: obtém o repositório da entidade. -- `Criteria()`: obtém critérios de pesquisa (SmartSearch) para a entidade. +- `Criteria()`: obtém critérios de pesquisa (SmartSearch). - `Save()` / `SaveAsync()`: persiste alterações (Unit of Work). - `SendAsync(command)`: executa comandos. - `QueryAsync(request)`: executa consultas síncronas (lista) ou assíncronas (stream). -Tarefas comuns (snippets que Copilot deve sugerir) -- CRUD básico com repositório +Referência rápida de API: +- Builder: `AddWorkContext()` → `ConfigureDbContextPool(...)` → `ConfigureModel(...)` → `ConfigureRepositories(...)` → `ConfigureSearches(...)` → `ConfigureCommands(...)` → `ConfigureQueries(...)` +- Registros por assembly: `AddRepositories(assembly)`, `ConfigureSearches(assembly)`, `ConfigureCommands(assembly)`, `ConfigureQueries(assembly)` + +--- + +## 5. Exemplos de uso + +CRUD básico com repositório: ```csharp var person = new Person { Name = "John" }; await context.Repository().AddAsync(person); @@ -56,86 +95,33 @@ await context.SaveAsync(); var found = await context.Repository().FindAsync(person.Id); ``` -- Query síncrona +Query síncrona: ```csharp var result = await context.QueryAsync(new GetPersons { Name = "John" }, ct); ``` -- Query assíncrona (stream) +Query assíncrona (stream): ```csharp await foreach (var p in context.QueryAsync(new StreamPersons { Name = "John" }, ct)) { /* ... */ } ``` -- Command +Command: ```csharp var res = await context.SendAsync(new CreatePerson { Name = "John" }, ct); ``` ---- - -## UnitOfWork (Abstrações) - -- Define contratos para controle transacional e persistência. -- O WorkContext implementa/estende `IUnitOfWork`, oferecendo uma API coesa. - -Principais operações -- `Save()` / `SaveAsync()`: salvamento de alterações. -- Integração com Repositórios (Add/Update/Delete/Find) e com Commands/Queries. - ---- - -## Repositories (Abstrações) - -- Contratos para o padrão Repository orientado a entidades. -- Consumidos pelo WorkContext para operações CRUD e consulta por ID. - -Operações típicas -- `Add(entity)` / `AddAsync(entity)` -- `Find(id)` / `FindAsync(id)` -- Suporte a ID fortemente tipado (`Id`), mapeamentos e DTOs. - ---- - -## Implementações com Entity Framework - -As seguintes bibliotecas fornecem integrações com EF Core e provedores específicos: -- `RoyalCode.WorkContext.EntityFramework`: integração base com EF Core. -- `RoyalCode.WorkContext.PostgreSql`: configuração orientada ao provider Npgsql. -- `RoyalCode.WorkContext.Sqlite`: configuração orientada ao provider SQLite. -- `RoyalCode.WorkContext.SqlServer`: configuração orientada ao provider SQL Server. - -Recursos comuns -- Builders e extensões para configurar o `DbContext` e registrar serviços. -- Utilitários de criação/semente (dev/test) e aplicação de mapeamentos por assembly. -- Registro de repositórios, buscas, comandos e queries. - -Exemplos de testes do repositório (adaptado) +Exemplo de configuração EF em memória (SQLite): ```csharp -// Configuração em memória SQLite services.AddSqliteInMemoryWorkContextDefault() .EnsureDatabaseCreated() .ConfigureModel(b => b.ApplyConfigurationsFromAssembly(typeof(PersonMapping).Assembly)) .ConfigureRepositories(c => c.Add()) .ConfigureSearches(c => c.Add()) .SeedDatabase(async db => { /* dados iniciais */ await db.SaveChangesAsync(); }); - -// Uso -var context = sp.GetRequiredService(); -var repo = context.Repository(); -await repo.AddAsync(new Person { Name = "John" }); -await context.SaveAsync(); ``` ---- - -## Tutorial: Configuração completa do WorkContext - -Abaixo um exemplo de módulo que aplica configurações completas (modelo, repositórios, buscas, comandos e queries) reutilizáveis: - +Módulo de configuração reutilizável: ```csharp -using Microsoft.EntityFrameworkCore; -using RoyalCode.WorkContext; - public static class MyModuleConfigureWorkContext { public static IWorkContextBuilder ConfigureMyModule(this IWorkContextBuilder builder) @@ -150,52 +136,19 @@ public static class MyModuleConfigureWorkContext } ``` -Uso com SQLite In-Memory (dev/test) - -```csharp -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using RoyalCode.WorkContext; - -ServiceCollection services = new(); - -services.AddWorkContext() - .ConfigureDbContextPool((sp, builder) => - { - var conn = new SqliteConnection("DataSource=:memory:"); - conn.Open(); - builder.UseSqlite(conn); - }) - .ConfigureMyModule() - .EnsureDatabaseCreated(); - -var sp = services.BuildServiceProvider(); -var ctx = sp.GetRequiredService(); -``` - -Prompts úteis para Copilot -- "Configure WorkContext com SQLite in‑memory, registre repositórios e crie um handler `CreatePerson`." -- "Mapeie uma query para listar `Person` por `Name` usando `ConfigureQueries`." -- "Adicione CommandDispatcher e envie o comando `CreatePerson`." -``` - -Uso com PostgreSQL +Providers (uso rápido): ```csharp +// PostgreSQL services.AddWorkContext() .ConfigureDbContextPool((sp, builder) => builder.UseNpgsql("Host=...;Database=...;Username=...;Password=...")) .ConfigureMyModule(); -``` -Uso com SQL Server -```csharp +// SQL Server services.AddWorkContext() .ConfigureDbContextPool((sp, builder) => builder.UseSqlServer("Server=...;Database=...;Integrated Security=True")) .ConfigureMyModule(); -``` -Uso com SQLite (arquivo) -```csharp +// SQLite (arquivo) services.AddWorkContext() .ConfigureDbContextPool((sp, builder) => builder.UseSqlite("Data Source=mydb.sqlite")) .ConfigureMyModule(); @@ -203,12 +156,10 @@ services.AddWorkContext() --- -## Commands e Queries (uso básico) +## 6. Commands e Queries (uso básico) -Commands +Commands: ```csharp -using RoyalCode.WorkContext.Commands; - public sealed class CreatePerson : ICommandRequest { public string Name { get; set; } = null!; @@ -223,47 +174,27 @@ public sealed class CreatePersonHandler : ICommandHandler } } -// Envio var result = await context.SendAsync(new CreatePerson { Name = "John" }, default); ``` -Nota: `Result` e `Result` (incluindo operações como `HasProblems`, `MapAsync`) pertencem a outra biblioteca. Consulte a documentação específica dessa biblioteca para detalhes. -``` - -Queries (lista) +Queries (lista): ```csharp -using Microsoft.EntityFrameworkCore; -using RoyalCode.WorkContext.Querying; - public sealed class GetPersons : IQueryRequest { public string? Name { get; set; } } -// Registro inline com builder builder.ConfigureQueries(c => { c.Handle(async (req, db, ct) => await db.Set().Where(p => p.Name == req.Name).ToListAsync(ct)); }); -// Execução var persons = await context.QueryAsync(new GetPersons { Name = "John" }, default); ``` -### Queries com IQueryHandler (handlers tipados) - -Implementando handlers tipados com Entity Framework (Entities) +Queries com `IQueryHandler`: ```csharp -using Microsoft.EntityFrameworkCore; -using RoyalCode.WorkContext.Querying; -using RoyalCode.WorkContext.EntityFramework.Querying; - -public sealed class GetPersons : IQueryRequest -{ - public string? Name { get; set; } -} - public sealed class GetPersonsHandler : IQueryHandler { public async Task> HandleAsync(GetPersons request, MyDbContext db, CancellationToken ct = default) @@ -273,21 +204,14 @@ public sealed class GetPersonsHandler : IQueryHandler() .ConfigureDbContextPool((sp, b) => b.UseSqlite("DataSource=:memory:")) - .ConfigureQueries(typeof(GetPersonsHandler).Assembly); // faz scan de IQueryHandler<,...> + .ConfigureQueries(typeof(GetPersonsHandler).Assembly); ``` -Implementando handlers com DTO (IQueryRequest) +Queries com DTO (`IQueryRequest`): ```csharp -using Microsoft.EntityFrameworkCore; -using RoyalCode.WorkContext.Querying; -using RoyalCode.WorkContext.EntityFramework.Querying; - public sealed class PersonDto { public int Id { get; set; } public string Name { get; set; } = null!; } public sealed class GetPersonsDto : IQueryRequest @@ -305,18 +229,13 @@ public sealed class GetPersonsDtoHandler : IQueryHandler { public string? Name { get; set; } @@ -334,25 +253,43 @@ await foreach (var p in context.QueryAsync(new StreamPersons { Name = "John" }, } ``` -Troubleshooting +Troubleshooting: - Handler não registrado: verifique `ConfigureCommands`/`ConfigureQueries` e assembly (`typeof(Handler).Assembly`). - Banco não criado em dev/test: use `EnsureDatabaseCreated()` após configurar o provider. -- Conexão SQLite in‑memory vazando: certifique‑se de manter a conexão aberta no escopo de vida do `DbContext`. -- Problemas de DI: valide `AddWorkContext()` foi chamado e `BuildServiceProvider()` após todas as configurações. -``` +- Conexão SQLite in‑memory: mantenha a conexão aberta no escopo do `DbContext`. +- DI: valide `AddWorkContext()` e crie o `ServiceProvider` após configurar. + +Nota: `Result`/`Result` pertencem a outra biblioteca (SmartProblems). Consulte `.docs/problems.md`. --- -## Boas práticas +## 7. Boas Práticas + - Centralize regras de negócio em Commands; use Queries para leitura eficiente. - Configure mapeamentos via assemblies para manter organização modular. -- Use `EnsureDatabaseCreated` e `SqliteInMemory` apenas em dev/test. -- Evite acessar o `DbContext` diretamente; prefira `IWorkContext` e seus serviços. +- Use `EnsureDatabaseCreated` e Sqlite in‑memory apenas em dev/test. +- Prefira `IWorkContext` e seus serviços ao acesso direto ao `DbContext`. - Registre repositórios somente para raízes de agregado quando aplicável. +- Padroneize buscas com SmartSearch e DTOs com SmartSelector. + +--- + +## 8. Resumo + +WorkContext unifica UnitOfWork, Repositories, Search, Commands e Queries sobre EF Core. Os contratos nas bibliotecas de abstração estruturam operações e a implementação em `RoyalCode.*.EntityFramework` fornece integração pronta para uso com provedores comuns. Configure via builder fluente e use os serviços para CRUD, buscas e execução de pipelines de aplicação. + +--- + +## 9. Instruções para Ferramentas de IA (GitHub Copilot) + +Diretrizes de geração: +- Use `AddWorkContext()` e encadeie `ConfigureDbContextPool`/`ConfigureModel`/`ConfigureRepositories`/`ConfigureSearches`/`ConfigureCommands`/`ConfigureQueries`. +- Gere comandos/queries com handlers e registre por assembly. +- Para CRUD, prefira `Repository()` e finalize com `SaveAsync()`. +- Ao usar Sqlite in‑memory, mantenha a conexão aberta. +- Para problemas de negócio, utilize `Result`/`Problems` (vide `.docs/problems.md`). -## Referência rápida -- Configurar WorkContext: `AddWorkContext().ConfigureDbContextPool(...).ConfigureModel(...).ConfigureRepositories(...).ConfigureSearches(...).ConfigureCommands(...).ConfigureQueries(...)`. -- Obter serviços: `IWorkContext`, `IRepository`, `ISearchManager`, `ICommandDispatcher`. -- Persistir: `SaveAsync()`. -- Enviar comando: `SendAsync(cmd)`. -- Executar consulta: `QueryAsync(request)`. +Padrões de prompt: +- "Configure WorkContext com SQLite in‑memory e crie o handler `CreatePerson` retornando `Result`." +- "Implemente query tipada por DTO e registre com `ConfigureQueries`." +- "Adicione repositórios e use `Repository()` para CRUD com `SaveAsync()`." diff --git a/RoyalCode.EnterprisePatterns/Directory.Build.props b/RoyalCode.EnterprisePatterns/Directory.Build.props index c959fd5..a8f07d5 100644 --- a/RoyalCode.EnterprisePatterns/Directory.Build.props +++ b/RoyalCode.EnterprisePatterns/Directory.Build.props @@ -6,10 +6,10 @@ - 0.8.1 + 0.8.2 - 0.8.9 + 0.8.13 0.1.0 @@ -19,24 +19,30 @@ -preview-0.1 - 8.0.23 + 8.0.11 8.0.2 - 8.0.10 + 8.0.11 + 8.0.11 + 8.0.11 9.0.0 9.0.0 + 9.0.0 9.0.0 + 9.0.0 10.0.0 10.0.0 + 10.0.0 10.0.0 + 10.0.0 1.0.0-preview-4.0 1.0.0-preview-6.0 1.0.0 - 0.10.0 + 0.10.5 diff --git a/RoyalCode.EnterprisePatterns/EnterprisePatterns.sln b/RoyalCode.EnterprisePatterns/EnterprisePatterns.sln index c63d97d..b19bb77 100644 --- a/RoyalCode.EnterprisePatterns/EnterprisePatterns.sln +++ b/RoyalCode.EnterprisePatterns/EnterprisePatterns.sln @@ -90,6 +90,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{36D7493B-A ProjectSection(SolutionItems) = preProject .docs\domain.md = .docs\domain.md .docs\instructions-for-copilot.md = .docs\instructions-for-copilot.md + .docs\problems.md = .docs\problems.md + .docs\search.md = .docs\search.md + .docs\selector.md = .docs\selector.md + .docs\validations.md = .docs\validations.md .docs\workcontext.md = .docs\workcontext.md EndProjectSection EndProject diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Automation/RoyalCode.Commands.Automation.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Automation/RoyalCode.Commands.Automation.csproj index 84e4369..90c9fb9 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Automation/RoyalCode.Commands.Automation.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Automation/RoyalCode.Commands.Automation.csproj @@ -22,7 +22,7 @@ - + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Tests/RoyalCode.Commands.Tests.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Tests/RoyalCode.Commands.Tests.csproj index 0efc907..2c59f2e 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Tests/RoyalCode.Commands.Tests.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Tests/RoyalCode.Commands.Tests.csproj @@ -3,8 +3,8 @@ - - + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.EntityFramework.StagedSaveChanges/RoyalCode.EntityFramework.StagedSaveChanges.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.EntityFramework.StagedSaveChanges/RoyalCode.EntityFramework.StagedSaveChanges.csproj index 65be205..525447e 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.EntityFramework.StagedSaveChanges/RoyalCode.EntityFramework.StagedSaveChanges.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.EntityFramework.StagedSaveChanges/RoyalCode.EntityFramework.StagedSaveChanges.csproj @@ -16,7 +16,7 @@ - + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.EntityFramework/RoyalCode.Events.Outbox.EntityFramework.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.EntityFramework/RoyalCode.Events.Outbox.EntityFramework.csproj index e436bed..5710dbf 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.EntityFramework/RoyalCode.Events.Outbox.EntityFramework.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.EntityFramework/RoyalCode.Events.Outbox.EntityFramework.csproj @@ -13,10 +13,12 @@ RoyalCode Enterprise-Patterns Events Outbox Outbox-Pattern + + + - diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/RoyalCode.OperationHint.Tests.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/RoyalCode.OperationHint.Tests.csproj index a7c4b9c..5700baf 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/RoyalCode.OperationHint.Tests.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/RoyalCode.OperationHint.Tests.csproj @@ -3,7 +3,9 @@ - + + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/RoyalCode.Persistence.Tests.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/RoyalCode.Persistence.Tests.csproj index f7532f4..749a7bd 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/RoyalCode.Persistence.Tests.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/RoyalCode.Persistence.Tests.csproj @@ -3,8 +3,8 @@ - - + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RoyalCode.Repositories.EntityFramework.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RoyalCode.Repositories.EntityFramework.csproj index a391b1a..f4e9ce1 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RoyalCode.Repositories.EntityFramework.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RoyalCode.Repositories.EntityFramework.csproj @@ -21,7 +21,7 @@ - + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/RoyalCode.UnitOfWork.EntityFramework.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/RoyalCode.UnitOfWork.EntityFramework.csproj index 04d31ab..d293e27 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/RoyalCode.UnitOfWork.EntityFramework.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/RoyalCode.UnitOfWork.EntityFramework.csproj @@ -17,8 +17,8 @@ - - + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/RoyalCode.WorkContext.SqlServer.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/RoyalCode.WorkContext.SqlServer.csproj index 1a0bc4b..e034c17 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/RoyalCode.WorkContext.SqlServer.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/RoyalCode.WorkContext.SqlServer.csproj @@ -19,7 +19,7 @@ - + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/RoyalCode.WorkContext.Sqlite.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/RoyalCode.WorkContext.Sqlite.csproj index 8003087..c183ab7 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/RoyalCode.WorkContext.Sqlite.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/RoyalCode.WorkContext.Sqlite.csproj @@ -19,7 +19,9 @@ - + + + diff --git a/RoyalCode.EnterprisePatterns/tests.targets b/RoyalCode.EnterprisePatterns/tests.targets index 946c1ef..579c0a5 100644 --- a/RoyalCode.EnterprisePatterns/tests.targets +++ b/RoyalCode.EnterprisePatterns/tests.targets @@ -1,6 +1,6 @@ - net10.0 + net10.0 enable enable false @@ -10,12 +10,12 @@ 10.0.2 - + - + - +