diff --git a/.github/workflows/domain-nuget.yml b/.github/workflows/domain-nuget.yml index 1d2bc83..f4c9176 100644 --- a/.github/workflows/domain-nuget.yml +++ b/.github/workflows/domain-nuget.yml @@ -18,7 +18,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' include-prerelease: true - name: Build and pack Entities diff --git a/.github/workflows/persistence-nuget.yml b/.github/workflows/persistence-nuget.yml index 112f906..5608be6 100644 --- a/.github/workflows/persistence-nuget.yml +++ b/.github/workflows/persistence-nuget.yml @@ -18,7 +18,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' include-prerelease: true - name: Build and pack Repositories Abstractions @@ -38,6 +38,15 @@ jobs: - name: Build and pack Work Context EntityFramework run: dotnet build ./RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/RoyalCode.WorkContext.EntityFramework.csproj -c Release - + + - name: Build and pack Work Context PostgreSql + run: dotnet build ./RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.PostgreSql/RoyalCode.WorkContext.PostgreSql.csproj -c Release + + - name: Build and pack Work Context Sqlite + run: dotnet build ./RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/RoyalCode.WorkContext.Sqlite.csproj -c Release + + - name: Build and pack Work Context SqlServer + run: dotnet build ./RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/RoyalCode.WorkContext.SqlServer.csproj -c Release + - name: Publish run: dotnet nuget push ./**/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/RoyalCode.EnterprisePatterns/.docs/domain.md b/RoyalCode.EnterprisePatterns/.docs/domain.md new file mode 100644 index 0000000..e5362be --- /dev/null +++ b/RoyalCode.EnterprisePatterns/.docs/domain.md @@ -0,0 +1,130 @@ +# RoyalCode Enterprise Patterns – Domínio (Entities e 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`. + +Escopo: +- `RoyalCode.Entities` +- `RoyalCode.Aggregates` + +Compatibilidade: múltiplos targets (.NET 8, .NET 9 e .NET 10) via `$(LibTargets)`. + +--- + +## 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. + +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`. +- `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 */ }` + +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 +```csharp +using RoyalCode.Entities; + +public class Person : Entity +{ + public string Name { get; set; } = null!; +} +``` + +Exemplo com `Entity` +```csharp +using RoyalCode.Entities; + +public class CatalogItem : Entity +{ + public string Name { get; set; } = null!; + // Code é herdado e tem set protegido + public CatalogItem(Guid id, string code, string name) + { + Id = id; + Code = code; + Name = name; + } +} +``` + +--- + +## RoyalCode.Aggregates + +Modelagem de Agregados (DDD). Define a raiz do agregado e integra a coleta de eventos de domínio. + +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. + +Exemplo +```csharp +using RoyalCode.Aggregates; +using RoyalCode.DomainEvents; // IDomainEvent + +public sealed class Order : AggregateRoot +{ + public string Number { get; private set; } + + public Order(string number) + { + Id = Guid.NewGuid(); + Number = number; + AddEvent(new OrderCreated(Id, Number)); + } +} + +public record OrderCreated(Guid OrderId, string Number) : IDomainEvent; +``` + +Exemplo com `AggregateRoot` +```csharp +using RoyalCode.Aggregates; +using RoyalCode.DomainEvents; + +public sealed class ProductAggregate : AggregateRoot +{ + public string Name { get; private set; } + + public ProductAggregate(string code, string name) + { + Id = Guid.NewGuid(); + Code = code; + Name = name; + AddEvent(new ProductCreated(Id, Code)); + } +} + +public record ProductCreated(Guid ProductId, string Code) : IDomainEvent; +``` + +--- + +## 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. +- Prefira `ISoftDeletable` e `IActiveState` para cenários de (des)ativação e exclusão lógica. + +Para integração com persistência, consulte `.docs/workcontext.md`. + diff --git a/RoyalCode.EnterprisePatterns/.docs/instructions-for-copilot.md b/RoyalCode.EnterprisePatterns/.docs/instructions-for-copilot.md new file mode 100644 index 0000000..e69de29 diff --git a/RoyalCode.EnterprisePatterns/.docs/workcontext.md b/RoyalCode.EnterprisePatterns/.docs/workcontext.md new file mode 100644 index 0000000..d95c1a5 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/.docs/workcontext.md @@ -0,0 +1,358 @@ +# 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. + +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`. + +Targets: .NET 8, .NET 9, .NET 10 (via `$(LibTargets)`). + +--- + +## WorkContext (Abstrações) + +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. + +Principais extensões de configuração (EntityFramework) +- `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). +- `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. + +Mapa de provedores EF Core (como Copilot deve sugerir) +- 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)` + +Operações principais +- `Repository()`: obtém o repositório da entidade. +- `Criteria()`: obtém critérios de pesquisa (SmartSearch) para a entidade. +- `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 +```csharp +var person = new Person { Name = "John" }; +await context.Repository().AddAsync(person); +await context.SaveAsync(); +var found = await context.Repository().FindAsync(person.Id); +``` + +- Query síncrona +```csharp +var result = await context.QueryAsync(new GetPersons { Name = "John" }, ct); +``` + +- Query assíncrona (stream) +```csharp +await foreach (var p in context.QueryAsync(new StreamPersons { Name = "John" }, ct)) { /* ... */ } +``` + +- 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) +```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: + +```csharp +using Microsoft.EntityFrameworkCore; +using RoyalCode.WorkContext; + +public static class MyModuleConfigureWorkContext +{ + public static IWorkContextBuilder ConfigureMyModule(this IWorkContextBuilder builder) + where TDbContext : DbContext + { + return builder.ConfigureModel(modelBuilder => modelBuilder.MapMyModule()) + .AddRepositories(typeof(MyModuleConfigureWorkContext).Assembly) + .ConfigureSearches(typeof(MyModuleConfigureWorkContext).Assembly) + .ConfigureCommands(typeof(MyModuleConfigureWorkContext).Assembly) + .ConfigureQueries(typeof(MyModuleConfigureWorkContext).Assembly); + } +} +``` + +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 +```csharp +services.AddWorkContext() + .ConfigureDbContextPool((sp, builder) => builder.UseNpgsql("Host=...;Database=...;Username=...;Password=...")) + .ConfigureMyModule(); +``` + +Uso com SQL Server +```csharp +services.AddWorkContext() + .ConfigureDbContextPool((sp, builder) => builder.UseSqlServer("Server=...;Database=...;Integrated Security=True")) + .ConfigureMyModule(); +``` + +Uso com SQLite (arquivo) +```csharp +services.AddWorkContext() + .ConfigureDbContextPool((sp, builder) => builder.UseSqlite("Data Source=mydb.sqlite")) + .ConfigureMyModule(); +``` + +--- + +## Commands e Queries (uso básico) + +Commands +```csharp +using RoyalCode.WorkContext.Commands; + +public sealed class CreatePerson : ICommandRequest +{ + public string Name { get; set; } = null!; +} + +public sealed class CreatePersonHandler : ICommandHandler +{ + public async Task HandleAsync(CreatePerson request, IWorkContext context, CancellationToken ct) + { + context.Add(new Person { Name = request.Name }); + return await context.SaveAsync(ct); + } +} + +// 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) +```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) +```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) + { + return await db.Set() + .Where(p => request.Name == null || p.Name == request.Name) + .ToListAsync(ct); + } +} +``` + +Registrando handlers por assembly +```csharp +services.AddWorkContext() + .ConfigureDbContextPool((sp, b) => b.UseSqlite("DataSource=:memory:")) + .ConfigureQueries(typeof(GetPersonsHandler).Assembly); // faz scan de IQueryHandler<,...> +``` + +Implementando handlers 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 +{ + public string? Name { get; set; } +} + +public sealed class GetPersonsDtoHandler : IQueryHandler +{ + public async Task> HandleAsync(GetPersonsDto request, MyDbContext db, CancellationToken ct = default) + { + return await db.Set() + .Where(p => request.Name == null || p.Name == request.Name) + .Select(p => new PersonDto { Id = p.Id, Name = p.Name }) + .ToListAsync(ct); + } +} +``` + +Execução +```csharp +var list = await context.QueryAsync(new GetPersons { Name = "John" }, ct); +var dtos = await context.QueryAsync(new GetPersonsDto { Name = "John" }, ct); +``` + +Queries (stream assíncrono) +```csharp +using RoyalCode.WorkContext.Querying; + +public sealed class StreamPersons : IAsyncQueryRequest +{ + public string? Name { get; set; } +} + +builder.ConfigureQueries(c => +{ + c.AsyncHandle((req, db, ct) => + db.Set().Where(p => p.Name == req.Name).AsAsyncEnumerable()); +}); + +await foreach (var p in context.QueryAsync(new StreamPersons { Name = "John" }, default)) +{ + // consumir stream +} +``` + +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. +``` + +--- + +## 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. +- Registre repositórios somente para raízes de agregado quando aplicável. + +## 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)`. diff --git a/RoyalCode.EnterprisePatterns/Directory.Build.props b/RoyalCode.EnterprisePatterns/Directory.Build.props index ac30575..c959fd5 100644 --- a/RoyalCode.EnterprisePatterns/Directory.Build.props +++ b/RoyalCode.EnterprisePatterns/Directory.Build.props @@ -1,32 +1,42 @@ - net8.0;net9.0 - net8.0;net9.0 - net8.0;net9.0 + net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0 - 1.0.0 - -preview-8.0 + 0.8.1 + - 1.0.0 - -preview-9.0 + 0.8.9 + 0.1.0 -preview-1 - 1.0.0 + 0.1.0 -preview-0.1 - - 8.0.0 - 9.0.0 + + 8.0.23 + 8.0.2 + 8.0.10 + + + 9.0.0 + 9.0.0 9.0.0 + + 10.0.0 + 10.0.0 + 10.0.0 + - 1.0.0-preview-2.0 - 1.0.0-preview-4.3 + 1.0.0-preview-4.0 + 1.0.0-preview-6.0 1.0.0 - 0.8.1 + 0.10.0 diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.EnterprisePatterns.sln b/RoyalCode.EnterprisePatterns/EnterprisePatterns.sln similarity index 82% rename from RoyalCode.EnterprisePatterns/RoyalCode.EnterprisePatterns.sln rename to RoyalCode.EnterprisePatterns/EnterprisePatterns.sln index fb29e5a..c63d97d 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.EnterprisePatterns.sln +++ b/RoyalCode.EnterprisePatterns/EnterprisePatterns.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.31911.260 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11104.47 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoyalCode.Repositories.Abstractions", "RoyalCode.Repositories.Abstractions\RoyalCode.Repositories.Abstractions.csproj", "{32ED862A-FFC6-40A7-8BBA-0807AC5B7BE5}" EndProject @@ -74,6 +74,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoyalCode.Events.Outbox.Ent EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoyalCode.WorkContext.AspNetCore", "RoyalCode.WorkContext.AspNetCore\RoyalCode.WorkContext.AspNetCore.csproj", "{E7AD2D84-8249-47DC-A400-93572C952755}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoyalCode.WorkContext.Sqlite", "RoyalCode.WorkContext.Sqlite\RoyalCode.WorkContext.Sqlite.csproj", "{63BD398D-0D71-4B28-87A8-E6D335730F9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoyalCode.WorkContext.PostgreSql", "RoyalCode.WorkContext.PostgreSql\RoyalCode.WorkContext.PostgreSql.csproj", "{430B93E7-7F7C-4DAB-95EF-BBEDD98338B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoyalCode.WorkContext.SqlServer", "RoyalCode.WorkContext.SqlServer\RoyalCode.WorkContext.SqlServer.csproj", "{80B13539-A3CB-4C66-B1E2-189B1FDC4D04}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Abstractions", "Abstractions", "{9E60E90A-95C1-4B4A-B7CD-ADDC0EBB53CF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EntityFramework", "EntityFramework", "{DF5DDC2D-A42A-402C-8633-06D9BA0C416B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosting", "Hosting", "{D579FD6B-0FFC-4735-A27D-C6E0594FE305}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{36D7493B-A101-41D3-9779-9FE82422D617}" + ProjectSection(SolutionItems) = preProject + .docs\domain.md = .docs\domain.md + .docs\instructions-for-copilot.md = .docs\instructions-for-copilot.md + .docs\workcontext.md = .docs\workcontext.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -172,23 +191,35 @@ Global {E7AD2D84-8249-47DC-A400-93572C952755}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7AD2D84-8249-47DC-A400-93572C952755}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7AD2D84-8249-47DC-A400-93572C952755}.Release|Any CPU.Build.0 = Release|Any CPU + {63BD398D-0D71-4B28-87A8-E6D335730F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63BD398D-0D71-4B28-87A8-E6D335730F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63BD398D-0D71-4B28-87A8-E6D335730F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63BD398D-0D71-4B28-87A8-E6D335730F9C}.Release|Any CPU.Build.0 = Release|Any CPU + {430B93E7-7F7C-4DAB-95EF-BBEDD98338B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {430B93E7-7F7C-4DAB-95EF-BBEDD98338B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {430B93E7-7F7C-4DAB-95EF-BBEDD98338B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {430B93E7-7F7C-4DAB-95EF-BBEDD98338B7}.Release|Any CPU.Build.0 = Release|Any CPU + {80B13539-A3CB-4C66-B1E2-189B1FDC4D04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80B13539-A3CB-4C66-B1E2-189B1FDC4D04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80B13539-A3CB-4C66-B1E2-189B1FDC4D04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80B13539-A3CB-4C66-B1E2-189B1FDC4D04}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {32ED862A-FFC6-40A7-8BBA-0807AC5B7BE5} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} + {32ED862A-FFC6-40A7-8BBA-0807AC5B7BE5} = {9E60E90A-95C1-4B4A-B7CD-ADDC0EBB53CF} {E6E2DDCC-0629-4BBF-98E8-EECA1CA7A8A1} = {3EED25DE-FBB1-4451-8F44-D072B43A2D66} {9D8033FC-7CB8-40E0-97B2-81A710F920D1} = {3EED25DE-FBB1-4451-8F44-D072B43A2D66} {9354412F-7A51-471A-A52E-86D766DE5ACA} = {3EED25DE-FBB1-4451-8F44-D072B43A2D66} {3EED25DE-FBB1-4451-8F44-D072B43A2D66} = {A54A32AB-DA08-4825-A246-DE5A8C69D588} {4847AB47-2B45-4A3A-BF3B-7F010F21ED86} = {A54A32AB-DA08-4825-A246-DE5A8C69D588} - {0F585941-ACDB-46FA-B909-EA1FE71C7509} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} + {0F585941-ACDB-46FA-B909-EA1FE71C7509} = {9E60E90A-95C1-4B4A-B7CD-ADDC0EBB53CF} {2636C6E2-AA9F-4948-BF46-D0772F73D265} = {D91CABB8-AE3F-4FE8-B8E3-DEDBD6C2EDB6} - {D067CEAD-BE9E-4121-9288-A876A0EAE098} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} - {61A13F69-7346-4115-B517-976BADE0B926} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} - {9B833B9C-92BB-43AB-A068-5C1317F08F82} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} - {95E28BC1-E43B-4BB0-8136-B27119509A24} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} + {D067CEAD-BE9E-4121-9288-A876A0EAE098} = {9E60E90A-95C1-4B4A-B7CD-ADDC0EBB53CF} + {61A13F69-7346-4115-B517-976BADE0B926} = {DF5DDC2D-A42A-402C-8633-06D9BA0C416B} + {9B833B9C-92BB-43AB-A068-5C1317F08F82} = {DF5DDC2D-A42A-402C-8633-06D9BA0C416B} + {95E28BC1-E43B-4BB0-8136-B27119509A24} = {DF5DDC2D-A42A-402C-8633-06D9BA0C416B} {A851D07A-3480-4505-AEB4-83A5B61092E4} = {8198A8B4-6CCB-4347-97C0-23AFFB879CA1} {8198A8B4-6CCB-4347-97C0-23AFFB879CA1} = {A54A32AB-DA08-4825-A246-DE5A8C69D588} {E3A5F862-236E-4F18-AF22-847F297DE7A3} = {DE4106C5-D631-4B42-AB27-0103F5705693} @@ -205,7 +236,14 @@ Global {31027174-F707-49FB-A051-3FBB3C6FFA35} = {80CCF203-3C6E-4B07-B011-04F7800EA52E} {79F643A3-9BFA-4200-A9FB-83A713A6C3D2} = {80CCF203-3C6E-4B07-B011-04F7800EA52E} {BDAFD007-FEB0-44B8-8A44-5C6F6E540B45} = {80CCF203-3C6E-4B07-B011-04F7800EA52E} - {E7AD2D84-8249-47DC-A400-93572C952755} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} + {E7AD2D84-8249-47DC-A400-93572C952755} = {D579FD6B-0FFC-4735-A27D-C6E0594FE305} + {63BD398D-0D71-4B28-87A8-E6D335730F9C} = {DF5DDC2D-A42A-402C-8633-06D9BA0C416B} + {430B93E7-7F7C-4DAB-95EF-BBEDD98338B7} = {DF5DDC2D-A42A-402C-8633-06D9BA0C416B} + {80B13539-A3CB-4C66-B1E2-189B1FDC4D04} = {DF5DDC2D-A42A-402C-8633-06D9BA0C416B} + {9E60E90A-95C1-4B4A-B7CD-ADDC0EBB53CF} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} + {DF5DDC2D-A42A-402C-8633-06D9BA0C416B} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} + {D579FD6B-0FFC-4735-A27D-C6E0594FE305} = {DD6336C0-C66B-44CD-9BEA-D9ED66EEC7BF} + {36D7493B-A101-41D3-9779-9FE82422D617} = {B6D9A130-5359-4A71-A97B-703364D34B97} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9FAC6C2E-62DE-4665-BB16-4E7D445FAB26} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Abstractions/ICreationHandler.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Abstractions/ICreationHandler.cs index a018ea9..f685e2b 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Abstractions/ICreationHandler.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Abstractions/ICreationHandler.cs @@ -1,5 +1,5 @@ using RoyalCode.SmartProblems; -using RoyalCode.WorkContext.Abstractions; +using RoyalCode.WorkContext; namespace RoyalCode.Commands.Abstractions; diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Abstractions/IValidationHandler.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Abstractions/IValidationHandler.cs index f3d2e01..d2e0c82 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Abstractions/IValidationHandler.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Abstractions/IValidationHandler.cs @@ -1,5 +1,5 @@ using RoyalCode.SmartProblems; -using RoyalCode.WorkContext.Abstractions; +using RoyalCode.WorkContext; namespace RoyalCode.Commands.Abstractions; diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Automation/RoyalCode.Commands.Automation.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Automation/RoyalCode.Commands.Automation.csproj index a1fb7ab..84e4369 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.Handlers/CreateCommandHandler.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Handlers/CreateCommandHandler.cs index 3768d37..0de6eeb 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Handlers/CreateCommandHandler.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Handlers/CreateCommandHandler.cs @@ -1,7 +1,7 @@ using RoyalCode.Commands.Abstractions; using RoyalCode.Entities; using RoyalCode.SmartProblems; -using RoyalCode.WorkContext.Abstractions; +using RoyalCode.WorkContext; namespace RoyalCode.Commands.Handlers; diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Tests/RoyalCode.Commands.Tests.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Commands.Tests/RoyalCode.Commands.Tests.csproj index bd41e40..0efc907 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 6496135..65be205 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.Abstractions/RoyalCode.Events.Outbox.Abstractions.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Abstractions/RoyalCode.Events.Outbox.Abstractions.csproj index a48d0fb..677d2ef 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Abstractions/RoyalCode.Events.Outbox.Abstractions.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Abstractions/RoyalCode.Events.Outbox.Abstractions.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Api/OutboxApi.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Api/OutboxApi.cs index 569f0b9..07806a5 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Api/OutboxApi.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Api/OutboxApi.cs @@ -37,17 +37,30 @@ public static void MapOutbox(this RouteGroupBuilder group) group.MapPost("consumer", RegisterConsumerAsync) .WithName("register-consumer") .WithDescription("Register a new outbox consumer") +#if NET10_0_OR_GREATER + ; + #else .WithOpenApi(); +#endif group.MapGet("consumer/{consumer}/messages", GetConsumerMessagesAsync) .WithName("get-outbox-messages") .WithDescription("get the outbox next messages for the consumer") +#if NET10_0_OR_GREATER + ; +#else .WithOpenApi(); +#endif group.MapPost("consumer/{consumer}/commit", CommitConsumedAsync) .WithName("commit-consumed-outbox-messages") .WithDescription("Commit the consumed messages") +#if NET10_0_OR_GREATER + ; +#else .WithOpenApi(); +#endif + } private static async Task CommitConsumedAsync( diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Api/RoyalCode.Events.Outbox.Api.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Api/RoyalCode.Events.Outbox.Api.csproj index a1117c4..a787185 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Api/RoyalCode.Events.Outbox.Api.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Events.Outbox.Api/RoyalCode.Events.Outbox.Api.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 dad5c65..e436bed 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 @@ -16,7 +16,7 @@ - + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/RoyalCode.OperationHint.Tests.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/RoyalCode.OperationHint.Tests.csproj index ee983ff..a7c4b9c 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/RoyalCode.OperationHint.Tests.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/RoyalCode.OperationHint.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/Searches/SearchesTestes.cs b/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/Searches/SearchesTestes.cs index e58e118..7975a98 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/Searches/SearchesTestes.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/Searches/SearchesTestes.cs @@ -1,6 +1,6 @@ using FluentAssertions; using Microsoft.Extensions.DependencyInjection; -using RoyalCode.WorkContext.Abstractions; +using RoyalCode.WorkContext; namespace RoyalCode.OperationHint.Tests.Searches; diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/Utils.cs b/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/Utils.cs index fa37100..c0a649d 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/Utils.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.OperationHint.Tests/Utils.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using RoyalCode.WorkContext.EntityFramework.Configurations; using System.Data.Common; namespace RoyalCode.OperationHint.Tests; @@ -10,7 +11,7 @@ internal static class Utils { public static TServices AddWorkContext( TServices services, - Action>? configureBuilder = null) + Action>? configureBuilder = null) where TServices : IServiceCollection { DbConnection conn = new SqliteConnection("Data Source=:memory:"); @@ -31,7 +32,7 @@ public static TServices AddWorkContext( public static TServices AddWorkContextWithIncludes( TServices services, - Action>? configureBuilder = null) + Action>? configureBuilder = null) where TServices : IServiceCollection { AddWorkContext(services, builder => diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/RoyalCode.Persistence.Tests.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/RoyalCode.Persistence.Tests.csproj index 8c66039..f7532f4 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/RoyalCode.Persistence.Tests.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/RoyalCode.Persistence.Tests.csproj @@ -3,14 +3,17 @@ - - + + + + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/UnitOfWork/UnitOfWorkBuilderTests.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/UnitOfWork/UnitOfWorkBuilderTests.cs index a9ec77a..1bf4b62 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/UnitOfWork/UnitOfWorkBuilderTests.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/UnitOfWork/UnitOfWorkBuilderTests.cs @@ -3,6 +3,7 @@ using RoyalCode.Persistence.Tests.Entities; using RoyalCode.Repositories; using RoyalCode.UnitOfWork; +using RoyalCode.UnitOfWork.EntityFramework; using Xunit; namespace RoyalCode.Persistence.Tests.UnitOfWork; @@ -38,6 +39,36 @@ public void ConfigureUnitOfWorkContextAndRepository() scope.Dispose(); } + + [Fact] + public void ConfigureUnitOfWorkFromAssembly() + { + ServiceCollection services = new(); + + services.AddUnitOfWorkDefault() + .ConfigureOptions(builder => builder.UseSqlite("DataSource=:memory:")) + .ConfigureMappingsFromAssembly(typeof(UnitOfWorkBuilderTests).Assembly, true); + + var root = services.BuildServiceProvider(); + var scope = root.CreateScope(); + var sp = scope.ServiceProvider; + + var db = sp.GetService(); + Assert.NotNull(db); + + db!.Database.EnsureCreated(); + + var uow = sp.GetService(); + Assert.NotNull(uow); + + var repo = sp.GetService>(); + Assert.NotNull(repo); + + var set = db.Set(); + Assert.NotNull(set); + + scope.Dispose(); + } } #region Test classes diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/WorkContext/RepositoryTests.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/WorkContext/RepositoryTests.cs new file mode 100644 index 0000000..284be8d --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/WorkContext/RepositoryTests.cs @@ -0,0 +1,149 @@ +using Microsoft.Extensions.DependencyInjection; +using RoyalCode.Persistence.Tests.Entities; +using RoyalCode.Repositories; +using RoyalCode.SmartProblems.Entities; +using RoyalCode.WorkContext; +using Xunit; + +namespace RoyalCode.Persistence.Tests.WorkContext; + +public class RepositoryTests +{ + private static ServiceCollection CreateServiceCollectionWithWorkContextConfigured() + { + ServiceCollection services = new(); + + services.AddSqliteInMemoryWorkContextDefault() + .EnsureDatabaseCreated() + .ConfigureModel(builder => builder.Entity().Property("Id").ValueGeneratedOnAdd()) + .ConfigureRepositories(builder => builder.Add()) + .SeedDatabase(async (db) => + { + db.Add(new Person { Name = "Alice" }); + db.Add(new Person { Name = "Bob" }); + await db.SaveChangesAsync(); + }); + + return services; + } + + [Fact] + public void GetRepositoryService() + { + // arrange + var services = CreateServiceCollectionWithWorkContextConfigured(); + var sp = services.BuildServiceProvider(); + + // act + var repository = sp.GetService>(); + + // assert + Assert.NotNull(repository); + } + + [Fact] + public void Find() + { + // arrange + var services = CreateServiceCollectionWithWorkContextConfigured(); + var sp = services.BuildServiceProvider(); + var repository = sp.GetRequiredService>(); + + // act + var person = repository.Find(1); + + // assert + Assert.NotNull(person); + } + + [Fact] + public async Task FindAsync() + { + // arrange + var services = CreateServiceCollectionWithWorkContextConfigured(); + var sp = services.BuildServiceProvider(); + var repository = sp.GetRequiredService>(); + + // act + var person = await repository.FindAsync(1); + + // assert + Assert.NotNull(person); + } + + [Fact] + public async Task FindAsync_Id() + { + // arrange + var services = CreateServiceCollectionWithWorkContextConfigured(); + var sp = services.BuildServiceProvider(); + var repository = sp.GetRequiredService>(); + Id id = 1; + + // act + var result = await repository.FindAsync(id); + var notFound = result.NotFound(out var problem); + + // assert + Assert.False(notFound); + Assert.Null(problem); + Assert.NotNull(result.Entity); + } + + [Fact] + public async Task FindAsync_Id_Dto() + { + // arrange + var services = CreateServiceCollectionWithWorkContextConfigured(); + var sp = services.BuildServiceProvider(); + var repository = sp.GetRequiredService>(); + Id id = 1; + + // act + var result = await repository.FindAsync(id); + var notFound = result.NotFound(out var problem); + + // assert + Assert.False(notFound); + Assert.Null(problem); + Assert.NotNull(result.Entity); + } + + [Fact] + public async Task Add() + { + // arrange + var services = CreateServiceCollectionWithWorkContextConfigured(); + var sp = services.BuildServiceProvider(); + var repository = sp.GetRequiredService>(); + var context = sp.GetRequiredService(); + + // act + var person = new Person { Name = "John Doe" }; + repository.Add(person); + await context.SaveAsync(); + + // assert + var foundPerson = await repository.FindAsync(person.Id); + Assert.NotNull(foundPerson); + } + + [Fact] + public async Task AddAsync() + { + // arrange + var services = CreateServiceCollectionWithWorkContextConfigured(); + var sp = services.BuildServiceProvider(); + var repository = sp.GetRequiredService>(); + var context = sp.GetRequiredService(); + + // act + var person = new Person { Name = "John Doe" }; + await repository.AddAsync(person); + await context.SaveAsync(); + + // assert + var foundPerson = await repository.FindAsync(person.Id); + Assert.NotNull(foundPerson); + } +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/WorkContext/WorkContextBuilderTests.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/WorkContext/WorkContextBuilderTests.cs index f31e66e..2b50351 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/WorkContext/WorkContextBuilderTests.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Persistence.Tests/WorkContext/WorkContextBuilderTests.cs @@ -1,5 +1,6 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.Extensions.DependencyInjection; using RoyalCode.Persistence.Tests.Entities; using RoyalCode.Repositories; @@ -73,6 +74,50 @@ public void ConfigureWorkContextAndRepositoryAndSearch() scope.Dispose(); } + + [Fact] + public async Task Sqlite_AddSqliteInMemoryWorkContextDefault() + { + ServiceCollection services = new(); + + services.AddSqliteInMemoryWorkContextDefault() + .EnsureDatabaseCreated() + .ConfigureModel(b => + { + b.ApplyConfigurationsFromAssembly(typeof(PersonMapping).Assembly); + }) + .ConfigureRepositories(c => + { + c.Add(); + }) + .ConfigureSearches(c => + { + c.Add(); + }); + + var root = services.BuildServiceProvider(); + var scope = root.CreateScope(); + var sp = scope.ServiceProvider; + + var workContext = sp.GetService(); + Assert.NotNull(workContext); + + var defaultRepo = workContext.Repository(); + Assert.NotNull(defaultRepo); + + var person = new Person { Name = "John Doe" }; + await defaultRepo.AddAsync(person); + await workContext.SaveAsync(); + + var contextCriteria = workContext.Criteria(); + Assert.NotNull(contextCriteria); + + var contextAllPersons = contextCriteria.Collect(); + Assert.NotNull(contextAllPersons); + Assert.NotEmpty(contextAllPersons); + + scope.Dispose(); + } } @@ -93,4 +138,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } +class PersonMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Persons"); + builder.HasKey(p => p.Id); + builder.Property(p => p.Name).IsRequired().HasMaxLength(100); + } +} + #endregion \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.Abstractions/Configurations/IRepositoriesBuilder.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.Abstractions/Configurations/IRepositoriesBuilder.cs index cae44bd..9c17dbb 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.Abstractions/Configurations/IRepositoriesBuilder.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.Abstractions/Configurations/IRepositoriesBuilder.cs @@ -15,6 +15,13 @@ public interface IRepositoriesBuilder IRepositoriesBuilder Add() where TEntity : class; + /// + /// Add a repository for an entity type as a service, related to builder used by the unit of work. + /// + /// The entity type. + /// The same instance. + IRepositoriesBuilder Add(Type entityType); + /// /// Allows the configuration of hints for repository operations. /// diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.Abstractions/DataServices.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.Abstractions/DataServices.cs index d3bdec1..f107d5e 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.Abstractions/DataServices.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.Abstractions/DataServices.cs @@ -129,11 +129,28 @@ public interface IFinder /// Cancellation token. /// /// - /// An entry representing the entity record obtained from the database. + /// A result representing the entity record obtained from the database. /// /// Task> FindAsync(Id id, CancellationToken ct = default); + /// + /// + /// Try to find an existing entity through its unique identity (Id) and select a DTO type to represent the entity. + /// + /// + /// Data transfer object type selected to represent the entity. + /// The type o entity id. + /// The entity id. + /// Cancellation token. + /// + /// + /// A result representing the DTO selected from the entity obtained from the database. + /// + /// + Task> FindAsync(Id id, CancellationToken ct = default) + where TDto : class; + /// /// /// Try to find an existing entity through a filter expression. diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Configurations/RepositoriesBuilder.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Configurations/RepositoriesBuilder.cs index 5a2b35b..4224177 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Configurations/RepositoriesBuilder.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Configurations/RepositoriesBuilder.cs @@ -31,12 +31,15 @@ public RepositoriesBuilder( } /// - public IRepositoriesBuilder Add() where TEntity : class + public IRepositoriesBuilder Add() where TEntity : class => Add(typeof(TEntity)); + + /// + public IRepositoriesBuilder Add(Type entityType) { // register the repository - var repoType = typeof(IRepository<>).MakeGenericType(typeof(TEntity)); - var dbRepoType = typeof(IRepository<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); - var repoImplType = typeof(InternalRepository<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); + var repoType = typeof(IRepository<>).MakeGenericType(entityType); + var dbRepoType = typeof(IRepository<,>).MakeGenericType(typeof(TDbContext), entityType); + var repoImplType = typeof(InternalRepository<,>).MakeGenericType(typeof(TDbContext), entityType); services.Add(ServiceDescriptor.Describe(dbRepoType, repoImplType, lifetime)); services.Add(ServiceDescriptor.Describe(repoType, sp => sp.GetService(dbRepoType)!, lifetime)); @@ -45,23 +48,23 @@ public IRepositoriesBuilder Add() where TEntity : class services.Add(ServiceDescriptor.Describe(dataService, sp => sp.GetService(dbRepoType)!, lifetime)); // if the entity implements IHasGuid interface, register the FinderByGuid - if (typeof(IHasGuid).IsAssignableFrom(typeof(TEntity))) + if (typeof(IHasGuid).IsAssignableFrom(entityType)) { - var finderType = typeof(IFinderByGuid<>).MakeGenericType(typeof(TEntity)); - var finderImplType = typeof(FinderByGuid<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); + var finderType = typeof(IFinderByGuid<>).MakeGenericType(entityType); + var finderImplType = typeof(FinderByGuid<,>).MakeGenericType(typeof(TDbContext), entityType); services.Add(ServiceDescriptor.Describe(finderType, finderImplType, lifetime)); } - + // if the entity implements IHasCode interface, register the FinderByCode - if (IsGenericAssinableFrom(typeof(IFinderByCode<,>), typeof(TEntity), out var finderByCodeType)) + if (IsGenericAssinableFrom(typeof(IFinderByCode<,>), entityType, out var finderByCodeType)) { var finderImplType = typeof(FinderByCode<,,>) - .MakeGenericType(typeof(TDbContext), typeof(TEntity), finderByCodeType.GetGenericArguments()[1]); + .MakeGenericType(typeof(TDbContext), entityType, finderByCodeType.GetGenericArguments()[1]); services.Add(ServiceDescriptor.Describe(finderByCodeType, finderImplType, lifetime)); } - + return this; } diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Extensions/EFExtensions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Extensions/EFExtensions.cs new file mode 100644 index 0000000..1175bbd --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Extensions/EFExtensions.cs @@ -0,0 +1,102 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using RoyalCode.Repositories.EntityFramework; +using RoyalCode.SmartProblems.Entities; + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Provides extension methods for querying DTOs from a using entity identifiers. +/// +public static class EFExtensions +{ + /// + /// Finds a DTO mapped from an entity by its identifier. + /// + /// The entity type. + /// The DTO type to project to. + /// The instance. + /// The identifier of the entity. + /// The DTO instance if found; otherwise, null. + public static TDto? Find( + this DbContext db, + object id) + where TEntity : class + where TDto : class + { + ArgumentNullException.ThrowIfNull(db); + ArgumentNullException.ThrowIfNull(id); + + return SelectDtoById.FindByIdAndSelectDto(db, id).FirstOrDefault(); + } + + /// + /// Asynchronously finds a DTO mapped from an entity by its identifier. + /// + /// The entity type. + /// The DTO type to project to. + /// The instance. + /// The identifier of the entity. + /// A cancellation token. + /// A task that represents the asynchronous operation. The task result contains the DTO instance if found; otherwise, null. + public static async Task FindAsync( + this DbContext db, + object id, + CancellationToken token = default) + where TEntity : class + where TDto : class + { + ArgumentNullException.ThrowIfNull(db); + ArgumentNullException.ThrowIfNull(id); + + return await SelectDtoById.FindByIdAndSelectDto(db, id).FirstOrDefaultAsync(token); + } + + /// + /// Asynchronously tries to find a DTO mapped from an entity by its identifier, returning an entry with the DTO and identifier. + /// + /// The entity type. + /// The DTO type to project to. + /// The type of the identifier. + /// The instance. + /// The identifier of the entity. + /// A cancellation token. + /// A task that represents the asynchronous operation. The task result contains an with the DTO and identifier. + public static async Task> TryFindAsync( + this DbContext db, + Id id, + CancellationToken token = default) + where TEntity : class + where TDto : class + { + ArgumentNullException.ThrowIfNull(db); + + var dto = await SelectDtoById.FindByIdAndSelectDto(db, id.Value!).FirstOrDefaultAsync(token); + + return new FindResult(dto, id.Value); + } + + /// + /// Gets a service registered in the application's service provider associated with the . + /// + /// The type of service to obtain. + /// Access to the service provider of the DbContext. + /// The requested service instance. + /// + /// If there is no service of type registered + /// in the application's service provider associated with the , + /// or when the application's service provider is not configured for the . + /// + public static TService GetApplicationService(this IInfrastructure accessor) + where TService : class + { + var sp = accessor.Instance; + + return (sp.GetService() + ?.Extensions.OfType().FirstOrDefault() + ?.ApplicationServiceProvider + ?.GetRequiredService()) + ?? throw new InvalidOperationException( + $"No service of type '{typeof(TService).FullName}' is registered in the DbContext."); + } +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/FilterById.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/FilterById.cs new file mode 100644 index 0000000..7417eba --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/FilterById.cs @@ -0,0 +1,88 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace RoyalCode.Repositories.EntityFramework; + +/// +/// +/// Utility class for applying filters to entities by ID in the context of the database. +/// +/// +/// The type of entity to be filtered. +public static class FilterById + where TEntity : class +{ + private static Func>>? entityFilterFactory; + + /// + /// Applies a filter to search for an entity by its ID in the context of the database. + /// + /// The database context for accessing the entity model and the DbSet. + /// The identifier of the entity to be filtered. + /// An IQueryable query that represents the filter applied to the entity's DbSet. + public static IQueryable Filter(DbContext db, object id) + { + entityFilterFactory ??= CreateEntityFilterFactory(db); + var filter = entityFilterFactory(id); + return db.Set().Where(filter); + } + + /// + /// Creates a filter expression to search for an entity by its ID. + /// + /// The database context for accessing the entity model. + /// The identifier of the entity to be filtered. + /// An expression that represents the filter for the entity that can be used in LINQ queries. + public static Expression> GetFilterExpression(DbContext db, object id) + { + entityFilterFactory ??= CreateEntityFilterFactory(db); + return entityFilterFactory(id); + } + + private static Func>> CreateEntityFilterFactory(DbContext db) + { + var entityType = db.Model.FindEntityType(typeof(TEntity)) + ?? throw new InvalidOperationException($"Entity type {typeof(TEntity).Name} not found in model."); + var keyProps = entityType.FindPrimaryKey()?.Properties + ?? throw new InvalidOperationException($"Entity {typeof(TEntity).Name} does not have a primary key."); + + return (idValue) => + { + var entityParam = Expression.Parameter(typeof(TEntity), "e"); + Expression? body = null; + if (keyProps.Count == 1) + { + var keyProp = keyProps[0]; + var entityProp = Expression.Property(entityParam, keyProp.Name); + var idConst = Expression.Constant(idValue, keyProp.ClrType); + body = Expression.Equal(entityProp, idConst); + } + else + { + var idObj = idValue; + var idType = idObj.GetType(); + Expression? composed = null; + for (int i = 0; i < keyProps.Count; i++) + { + var keyProp = keyProps[i]; + var entityProp = Expression.Property(entityParam, keyProp.Name); + object partValue; + if (idType.IsArray) + { + partValue = ((object[])idObj)[i]; + } + else + { + var tupleProp = idType.GetProperty($"Item{i + 1}")!; + partValue = tupleProp.GetValue(idObj)!; + } + var idConst = Expression.Constant(partValue, keyProp.ClrType); + var eq = Expression.Equal(entityProp, idConst); + composed = composed == null ? eq : Expression.AndAlso(composed, eq); + } + body = composed!; + } + return Expression.Lambda>(body!, entityParam); + }; + } +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Repository.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Repository.cs index 3fda878..0b7f0e3 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Repository.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/Repository.cs @@ -77,6 +77,13 @@ public async Task> FindAsync(Id id, return result; } + /// + public Task> FindAsync(Id id, CancellationToken ct = default) + where TDto : class + { + return db.TryFindAsync(id, ct); + } + /// public async Task> FindAsync(Expression> filter, CancellationToken ct = default) { diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RepositoryBuilderExtensions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RepositoryBuilderExtensions.cs new file mode 100644 index 0000000..805ade0 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RepositoryBuilderExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using RoyalCode.Repositories.EntityFramework.Configurations; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Provides extension methods for registering repositories in the dependency injection container. +/// +public static class RepositoryBuilderExtensions +{ + /// + /// Registers repositories for the specified in the service collection. + /// + /// The type of the to associate with the repositories. + /// The to add the repositories to. + /// An action to configure the repositories using an . + /// The for the registered repositories. Defaults to . + /// + /// The same instance for chaining. + /// + /// + /// Thrown if is null. + /// + public static IServiceCollection AddRepositories( + this IServiceCollection services, + Action> configureAction, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TDbContext : DbContext + { + ArgumentNullException.ThrowIfNull(configureAction); + var repositoryConfigurer = new RepositoriesBuilder(services, lifetime); + configureAction(repositoryConfigurer); + return services; + } +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RoyalCode.Repositories.EntityFramework.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RoyalCode.Repositories.EntityFramework.csproj index 2bcbdcb..a391b1a 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RoyalCode.Repositories.EntityFramework.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/RoyalCode.Repositories.EntityFramework.csproj @@ -21,9 +21,10 @@ - + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/SelectDtoById.cs b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/SelectDtoById.cs new file mode 100644 index 0000000..c07b483 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.Repositories.EntityFramework/SelectDtoById.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore; +using RoyalCode.SmartSearch.Linq.Services; +using System.Linq.Expressions; + +namespace RoyalCode.Repositories.EntityFramework; + +/// +/// Utility class to search for DTOs mapped from entities by ID in the context of the database. +/// +/// Type of entity to be filtered. +/// Type of DTO to be designed. +public static class SelectDtoById + where TEntity : class + where TDto : class +{ + private static Expression>? selectorExpression; + + /// + /// Applies a filter to search for an entity by its ID in the context of the database, + /// projecting the result to a DTO. + /// + /// The database context for accessing the entity model and the DbSet. + /// The identifier of the entity to be filtered. + /// An IQueryable that represents the filter applied to the entity's DbSet, designed for the DTO. + public static IQueryable FindByIdAndSelectDto(DbContext db, object id) + { + selectorExpression ??= CreateSelector(db); + return FilterById.Filter(db, id).Select(selectorExpression); + } + + /// + /// + /// Gets the selection expression to project an entity into a DTO. + /// + /// + /// The service from the database context will be used. + /// If it is not registered, an exception will be thrown. + /// + /// + /// The database context. + /// An expression that represents the selection of DTO from the entity. + public static Expression> GetSelector(DbContext db) + { + selectorExpression ??= CreateSelector(db); + return selectorExpression; + } + + private static Expression> CreateSelector(DbContext db) + { + ISelectorFactory selectorFactory = db.GetApplicationService() + ?? throw new InvalidOperationException( + "ISelectorFactory not found in service provider of the DbContext"); + + var selector = selectorFactory.Create() + ?? throw new InvalidOperationException( + $"Selector for {typeof(TEntity).Name} to {typeof(TDto).Name} not found."); + + return selector.GetSelectExpression(); + } +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.Abstractions/RoyalCode.UnitOfWork.Abstractions.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.Abstractions/RoyalCode.UnitOfWork.Abstractions.csproj index 5a9aabc..5850e8e 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.Abstractions/RoyalCode.UnitOfWork.Abstractions.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.Abstractions/RoyalCode.UnitOfWork.Abstractions.csproj @@ -17,7 +17,7 @@ - + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.Abstractions/SaveResult.cs b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.Abstractions/SaveResult.cs index 44f667a..2910cd2 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.Abstractions/SaveResult.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.Abstractions/SaveResult.cs @@ -1,4 +1,5 @@ using RoyalCode.SmartProblems; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -53,6 +54,7 @@ public SaveResult(int changes) public SaveResult(Exception ex) { Problems = Problems.InternalError(ex); + Exception = ex; } /// @@ -65,6 +67,11 @@ public SaveResult(Exception ex) /// public Problems? Problems { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + /// + /// The exception that occurred during the save operation, if any. + /// + public Exception? Exception { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } + /// /// Returns true if the save operation succeeded. /// @@ -112,6 +119,23 @@ public bool IsSuccessOrGetProblems([NotNullWhen(false)] out Problems? problems) return IsSuccess; } + /// + /// Validates the result and throws an exception if it's a failure. + /// + /// + /// Thrown when the save operation failed, either due to an exception or problems. + /// + [StackTraceHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void EnsureSuccess() + { + if (Exception is not null) + throw new InvalidOperationException($"Save operation failed with exception: {Exception.Message}", Exception); + + if (IsFailure) + throw Problems.ToException("Save operation failed with problem(s):\n - {0}", "\n - "); + } + /// /// Convert the save result to an operation result. /// diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IConfigureConventions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IConfigureConventions.cs new file mode 100644 index 0000000..f8cea54 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IConfigureConventions.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; + +namespace RoyalCode.UnitOfWork.EntityFramework.Configurations; + +/// +/// Defines a method to configure conventions for a using a . +/// +/// Tipo do DbContext. +public interface IConfigureConventions + where TDbContext : DbContext +{ + /// + /// Configures conventions for the specified + /// using the provided . + /// + /// The builder used to configure conventions for the . + void Configure(ModelConfigurationBuilder builder); +} \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IConfigureModel.cs b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IConfigureModel.cs new file mode 100644 index 0000000..e764d14 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IConfigureModel.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; + +namespace RoyalCode.UnitOfWork.EntityFramework.Configurations; + +/// +/// Defines a contract for configuring the of a . +/// +public interface IConfigureModel + where TDbContext : DbContext +{ + /// + /// Configures the for the specified . + /// + /// The to configure. + public void Configure(ModelBuilder modelBuilder); +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IConfigureOptions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IConfigureOptions.cs new file mode 100644 index 0000000..904ae04 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IConfigureOptions.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; + +namespace RoyalCode.UnitOfWork.EntityFramework.Configurations; + +/// +/// Defines a method to configure options for a using a . +/// +/// Tipo do DbContext. +public interface IConfigureOptions + where TDbContext : DbContext +{ + /// + /// Configures the options for the using the provided . + /// + /// The to configure. + void Configure(DbContextOptionsBuilder optionsBuilder); +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IUnitOfWorkBuilder.cs b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IUnitOfWorkBuilder.cs index b126b90..f02f882 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IUnitOfWorkBuilder.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Configurations/IUnitOfWorkBuilder.cs @@ -1,7 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using RoyalCode.UnitOfWork.Configurations; -using RoyalCode.UnitOfWork.EntityFramework.Services; +using System.Reflection; namespace RoyalCode.UnitOfWork.EntityFramework.Configurations; @@ -38,35 +40,79 @@ public interface IUnitOfWorkBuilder : IUnitOfWorkB /// Configure the for the unit of work. /// /// - /// The configuration is done by the + /// The configuration is done by the /// registered in the services. /// /// - /// When the is not registered, an + /// When the is not registered, an /// is thrown. /// /// + /// + /// This method call AddDbContext with the of the unit of work. + /// /// The same instance. /// - /// The is not registered. + /// The is not registered. /// - public TBuilder ConfigureWithService() + public TBuilder ConfigureDbContextWithService() { Services.AddDbContext((sp, builder) => { - var configurator = sp.GetService>(); + var configurators = sp.GetService>>(); - if (configurator is null) + if (configurators is null || !configurators.Any()) throw new InvalidOperationException( - "The IConfigureDbContextService is not registered. " + + "The IConfigureOptions is not registered. " + "When using the ConfigureWithService method, it is necessary to register the " + - "IConfigureDbContextService."); + "IConfigureOptions."); + + foreach (var configurator in configurators) + configurator.Configure(builder); - configurator.ConfigureDbContext(builder); }, Lifetime); return (TBuilder)this; } + /// + /// + /// Configure the for the unit of work. + /// + /// + /// The configuration is done by the + /// registered in the services. + /// + /// + /// When the is not registered, an + /// is thrown. + /// + /// + /// + /// This method call AddDbContextPool. + /// + /// The same instance. + /// + /// The is not registered. + /// + public TBuilder ConfigureDbContextPoolWithService() + { + Services.AddDbContextPool((sp, builder) => + { + var configurators = sp.GetService>>(); + + if (configurators is null || !configurators.Any()) + throw new InvalidOperationException( + "The IConfigureOptions is not registered. " + + "When using the ConfigureWithService method, it is necessary to register the " + + "IConfigureOptions."); + + foreach (var configurator in configurators) + configurator.Configure(builder); + + }); + return (TBuilder)this; + } + /// /// Configure the for the unit of work as pooled. /// @@ -113,4 +159,221 @@ public TBuilder ConfigureDbContext(Action(configurer, Lifetime); return (TBuilder)this; } + + /// + /// Adds an action to configure the of a . + /// + /// Action to configure. + /// The same instance. + public TBuilder ConfigureModel(Action configure) + { + InternalConfigureModel.GetFromServices(Services).Configure(configure); + return (TBuilder)this; + } + + /// + /// Adds an action to configure the of a . + /// + /// Action to configure. + /// The same instance. + public TBuilder ConfigureOptions(Action configure) + { + ConfigureOptionsAction configureOptions = (sp, builder) => configure(builder); + Services.AddSingleton(configureOptions); + Services.TryAddEnumerable(ServiceDescriptor.Transient, InternalConfigureOptions>()); + return (TBuilder)this; + } + + /// + /// Adds an action to configure the of a . + /// + /// Action to configure. + /// The same instance. + public TBuilder ConfigureOptions(Action configure) + { + ConfigureOptionsAction configureOptions = (sp, builder) => configure(sp, builder); + Services.AddSingleton(configureOptions); + Services.TryAddEnumerable(ServiceDescriptor.Transient, InternalConfigureOptions>()); + return (TBuilder)this; + } + + /// + /// Adds an action to configure the which can apply + /// convention configurations for a . + /// + /// Action to configure. + /// The same instance. + public TBuilder ConfigureConventions(Action configure) + { + var service = new InternalConfigureConventions(configure); + Services.AddSingleton>(service); + return (TBuilder)this; + } + + /// + /// Adds an action to configure the + /// to turns on the creation of lazy loading proxies. + /// + /// The same instance. + public TBuilder UseLazyLoadingProxies() + { + return ConfigureOptions(static ob => ob.UseLazyLoadingProxies()); + } + + /// + /// Adds an action to configure the to use a logger factory + /// and when is true, enables sensitive data logging and detailed errors. + /// + /// Determines whether to enable sensitive data logging and detailed errors. + /// The same instance. + public TBuilder UseLoggerFactoryAndEnableSensitiveDataLogging(bool isDevelopment = false) + { + return ConfigureOptions((sp, ob) => + { + if (isDevelopment) + { + ob.EnableSensitiveDataLogging(); + ob.EnableDetailedErrors(); + } + ob.UseLoggerFactory(sp.GetRequiredService()); + }); + } + + /// + /// Adds an action to configure the and applies all entity configurations + /// from the specified assembly. + /// Additionally, if is true, it will also add repositories + /// for the configured entities. + /// + /// The assembly from which to get the entity configurations. + /// Determines whether to add repositories for the configured entities. + /// The same instance. + public TBuilder ConfigureMappingsFromAssembly(Assembly assembly, bool addRepositories) + { + if (addRepositories) + AddRepositories(assembly); + return ConfigureModel(modelBuilder => + { + modelBuilder.ApplyConfigurationsFromAssembly(assembly); + }); + } + + /// + /// Adds an action to configure the and applies all entity configurations + /// from the assembly containing the specified type. + /// Additionally, if is true, it will also add repositories + /// for the configured entities. + /// + /// Type from which to get the assembly. + /// Determines whether to add repositories for the configured entities. + /// The same instance. + public TBuilder ConfigureMappingsFromAssembly(bool addRepositories) + where TTypeFromAssembly : class + { + return ConfigureMappingsFromAssembly(typeof(TTypeFromAssembly).Assembly, addRepositories); + } + + /// + /// + /// Adds repositories for all entity types found in the specified assembly. + /// + /// + /// Will scan the assembly for types that implement , + /// so the entity type will be the generic type argument of the interface. + /// + /// + /// For each entity type found, it will be added a repository related to the unit of work. + /// + /// + /// + /// + public TBuilder AddRepositories(Assembly assembly) + { + // gets all entities types from the assembly getting the configurations (IEntityTypeConfiguration<>) + var entityTypes = assembly.GetTypes() + .Where(type => type.IsClass && !type.IsAbstract) + .SelectMany(type => type.GetInterfaces()) + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>)) + .Select(i => i.GetGenericArguments()[0]); + + // adds repositories for each entity type + foreach (var type in entityTypes) + { + ConfigureRepositories(r => r.Add(type)); + } + + return (TBuilder)this; + } +} + +internal sealed class InternalConfigureOptions : IConfigureOptions + where TDb : DbContext +{ + private readonly IServiceProvider sp; + + public InternalConfigureOptions(IServiceProvider sp) + { + this.sp = sp; + } + + public void Configure(DbContextOptionsBuilder optionsBuilder) + { + var configurations = sp.GetRequiredService>>(); + foreach (var action in configurations) + { + action(sp, optionsBuilder); + } + } +} + +internal delegate void ConfigureOptionsAction(IServiceProvider sp, DbContextOptionsBuilder optionsBuilder) + where TDb : DbContext; + +internal sealed class InternalConfigureModel : IConfigureModel + where TDb : DbContext +{ + public static InternalConfigureModel GetFromServices(IServiceCollection services) + { + var descriptor = services.FirstOrDefault(d => d.ImplementationType == typeof(InternalConfigureModel)); + if (descriptor is null || descriptor.ImplementationInstance is not InternalConfigureModel options) + { + options = new InternalConfigureModel(); + services.AddSingleton>(options); + } + return options; + } + + private Action? configure; + + public void Configure(ModelBuilder modelBuilder) + { + if (configure is null) + return; + + configure(modelBuilder); + } + + public void Configure(Action configure) + { + if (this.configure is null) + this.configure = configure; + else + this.configure += configure; + } +} + +internal sealed class InternalConfigureConventions : IConfigureConventions + where TDb : DbContext +{ + private readonly Action configure; + + public InternalConfigureConventions(Action configure) + { + this.configure = configure; + } + + public void Configure(ModelConfigurationBuilder builder) + { + configure(builder); + } } \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/DefaultDbContext.cs b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/DefaultDbContext.cs new file mode 100644 index 0000000..37ecb3e --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/DefaultDbContext.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; + +namespace RoyalCode.UnitOfWork.EntityFramework; + +/// +/// Represents the default Entity Framework used for database operations. +/// +public sealed class DefaultDbContext : DbContext +{ + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The options to be used by the . + public DefaultDbContext(DbContextOptions options) : base(options) { } + + /// + /// Configures the model for this context by invoking custom model configuration logic and the base implementation. + /// + /// The builder being used to construct the model for this context. + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + this.ConfigureModelWithServices(modelBuilder); + + base.OnModelCreating(modelBuilder); + } + + /// + /// Configures conventions for this context by invoking custom convention configuration logic and the base implementation. + /// + /// The builder used to configure conventions for this context. + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + this.ConfigureConventionsWithServices(configurationBuilder); + + base.ConfigureConventions(configurationBuilder); + } +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Extensions/DbContextExtensions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Extensions/DbContextExtensions.cs new file mode 100644 index 0000000..545471f --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Extensions/DbContextExtensions.cs @@ -0,0 +1,47 @@ +using RoyalCode.UnitOfWork.EntityFramework.Configurations; + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Extension methods for . +/// +public static class DbContextExtensions +{ + /// + /// Configures a using services + /// obtained from the . + /// + /// The type of DbContext. + /// The DbContext. + /// The ModelBuilder of the DbContext. + public static void ConfigureModelWithServices( + this TDbContext context, ModelBuilder modelBuilder) + where TDbContext : DbContext + { + var configurations = context.GetApplicationService>>(); + if (configurations is null) + return; + + foreach (var configuration in configurations) + configuration.Configure(modelBuilder); + } + + /// + /// Configures a using + /// services obtained from the . + /// + /// The type of DbContext. + /// The DbContext. + /// The configuration builder of the DbContext. + public static void ConfigureConventionsWithServices( + this TDbContext context, ModelConfigurationBuilder configurationBuilder) + where TDbContext : DbContext + { + var configurations = context.GetApplicationService>>(); + if (configurations is null) + return; + + foreach (var configuration in configurations) + configuration.Configure(configurationBuilder); + } +} \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Extensions/UnitOfWorkServiceCollectionExtensions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Extensions/UnitOfWorkServiceCollectionExtensions.cs index 68cdc16..0520977 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Extensions/UnitOfWorkServiceCollectionExtensions.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Extensions/UnitOfWorkServiceCollectionExtensions.cs @@ -1,4 +1,3 @@ - using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection.Extensions; using RoyalCode.UnitOfWork.EntityFramework; @@ -12,6 +11,20 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class UnitOfWorkServiceCollectionExtensions { + /// + /// Adds a unit of work related to a default , + /// and configure the with services. + /// + /// The service collection. + /// The services lifetime, by default is scoped. + /// A unit of work builder to configure the and services like repositories and searches. + public static IUnitOfWorkBuilder AddUnitOfWorkDefault(this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddUnitOfWork(lifetime) + .ConfigureDbContextWithService(); + } + /// /// Adds a unit of work related to a . /// diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/RoyalCode.UnitOfWork.EntityFramework.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/RoyalCode.UnitOfWork.EntityFramework.csproj index a08154d..04d31ab 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/RoyalCode.UnitOfWork.EntityFramework.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/RoyalCode.UnitOfWork.EntityFramework.csproj @@ -17,7 +17,8 @@ - + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Services/IConfigureDbContextService.cs b/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Services/IConfigureDbContextService.cs deleted file mode 100644 index 92e4449..0000000 --- a/RoyalCode.EnterprisePatterns/RoyalCode.UnitOfWork.EntityFramework/Services/IConfigureDbContextService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace RoyalCode.UnitOfWork.EntityFramework.Services; - -/// -/// A service that configures a . -/// -/// The type of the . -public interface IConfigureDbContextService - where TDbContext : DbContext -{ - /// - /// Applies the configuration to the . - /// - /// The . - public void ConfigureDbContext(DbContextOptionsBuilder builder); -} \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Abstractions/Configurations/IWorkContextBuilderBase.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Abstractions/Configurations/IWorkContextBuilderBase.cs index 82bbb79..da6fb3a 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Abstractions/Configurations/IWorkContextBuilderBase.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Abstractions/Configurations/IWorkContextBuilderBase.cs @@ -17,6 +17,6 @@ namespace RoyalCode.WorkContext.Configurations; /// /// The type of the builder implementing this interface, enabling fluent configuration. /// -public interface IWorkContextBuilderBase : IUnitOfWorkBuilderBase +public interface IWorkContextBuilderBase : IUnitOfWorkBuilderBase where TBuilder : IWorkContextBuilderBase { } diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Configurations/IConfigureWorkContext.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Configurations/IConfigureWorkContext.cs new file mode 100644 index 0000000..aaa8ac1 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Configurations/IConfigureWorkContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +namespace RoyalCode.WorkContext.EntityFramework.Configurations; + +/// +/// Defines a contract for configuring a work context builder for a specific Entity Framework database context type. +/// +/// +/// Implementations of this interface can be used to customize the setup or behavior of work contexts in +/// applications that use Entity Framework. +/// +/// +/// The type of the Entity Framework database context to be used with the work context builder. +/// Must derive from . +/// +public interface IConfigureWorkContext + where TDbContext : DbContext +{ + /// + /// Applies custom configurations to the provided work context builder. + /// + /// The work context builder to be configured. + /// The configured work context builder. + public IWorkContextBuilder ConfigureWorkContext(IWorkContextBuilder builder); +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Configurations/IWorkContextBuilder.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Configurations/IWorkContextBuilder.cs index 13f2ced..08b0944 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Configurations/IWorkContextBuilder.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Configurations/IWorkContextBuilder.cs @@ -15,7 +15,7 @@ namespace RoyalCode.WorkContext.EntityFramework.Configurations; /// The type of the database context that the work context will be associated with. Must derive from /// . /// -public interface IWorkContextBuilder : IWorkContextBuilder> +public interface IWorkContextBuilder : IWorkContextBuilder> where TDbContext : DbContext { } @@ -34,7 +34,7 @@ public interface IWorkContextBuilder : IWorkContextBuilder /// The type of the builder implementing this interface, allowing for fluent method chaining. /// -public interface IWorkContextBuilder : IWorkContextBuilderBase, IUnitOfWorkBuilder +public interface IWorkContextBuilder : IWorkContextBuilderBase, IUnitOfWorkBuilder where TDbContext : DbContext where TBuilder : IWorkContextBuilder { diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Extensions/WorkContextBuilderExtensions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Extensions/WorkContextBuilderExtensions.cs new file mode 100644 index 0000000..fabfeb5 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Extensions/WorkContextBuilderExtensions.cs @@ -0,0 +1,135 @@ +using Microsoft.EntityFrameworkCore; +using RoyalCode.Entities; +using System.Reflection; + +namespace RoyalCode.WorkContext.EntityFramework.Configurations; + +/// +/// Exntesion methods for . +/// +public static class WorkContextBuilderExtensions +{ + /// + /// Apply configuration to the work context builder. + /// + /// The type of the DbContext. + /// The work context builder. + /// The configuration to apply. + /// The work context builder. + public static IWorkContextBuilder Configure( + this IWorkContextBuilder builder, IConfigureWorkContext configure) + where TDbContext : DbContext + { + return configure.ConfigureWorkContext(builder); + } + + /// + /// Configures the work context builder using a new instance of the specified work context configurer. + /// + /// + /// The type of the Entity Framework database context to be used in the work context. + /// + /// + /// The type of the work context configurer to apply. Must implement + /// and have a parameterless constructor. + /// + /// The work context builder to configure. + /// The configured work context builder instance. + public static IWorkContextBuilder Configure( + this IWorkContextBuilder builder) + where TDbContext : DbContext + where TConfigurer : IConfigureWorkContext, new() + { + var configure = new TConfigurer(); + return configure.ConfigureWorkContext(builder); + } + + /// + /// Configures the work context builder to register command handlers from the specified assembly. + /// + /// + /// The type of the database context used by the work context builder. Must inherit from . + /// + /// The work context builder to configure with command handlers. + /// The assembly from which command handler types will be discovered and registered. + /// The configured work context builder instance, enabling further configuration or building of the work context. + public static IWorkContextBuilder ConfigureCommands( + this IWorkContextBuilder builder, Assembly assembly) + where TDbContext : DbContext + { + return builder.ConfigureCommands(c => c.AddHandlersFromAssembly(assembly)); + } + + /// + /// Configures the work context builder to register query handlers from the specified assembly. + /// + /// + /// The type of the database context used by the work context builder. Must inherit from . + /// + /// The work context builder to configure with query handlers. + /// The assembly from which query handler types will be discovered and registered. + /// The configured work context builder instance, enabling further configuration or building of the work context. + public static IWorkContextBuilder ConfigureQueries( + this IWorkContextBuilder builder, Assembly assembly) + where TDbContext : DbContext + { + return builder.ConfigureQueries(c => c.AddHandlersFromAssembly(assembly)); + } + + /// + /// Configures search criteria for all entity types in the specified assembly that implement the interface. + /// + /// The type of the Entity Framework database context. + /// The work context builder to configure with search criteria. + /// The assembly containing entity types to register for search criteria. Only types implementing are + /// considered. + /// The same work context builder instance, configured with search criteria for the discovered entity types. + public static IWorkContextBuilder ConfigureSearches( + this IWorkContextBuilder builder, Assembly assembly) + where TDbContext : DbContext + { + // Configure ICriteria for all entities from the specified assembly + // read all entity classes from the assembly and register search criteria + // the entity class must implement the IEntity interface + var entityTypes = assembly.GetTypes() + .Where(t => t.GetInterfaces().Any(i => i == typeof(IEntity))) + .ToList(); + + return builder.ConfigureSearches(s => + { + foreach (var entityType in entityTypes) + { + s.Add(entityType); + } + }); + } + + /// + /// Configures repositories for all entity types in the specified assembly that implement the interface. + /// + /// The type of the database context for which repositories are being configured. Must inherit from DbContext. + /// The work context builder used to configure repositories for the specified database context. + /// The assembly containing entity types to be registered as repositories. Only types implementing are + /// considered. + /// The same work context builder instance, configured with repositories for all applicable entity types found in + /// the assembly. + public static IWorkContextBuilder ConfigureRepositories( + this IWorkContextBuilder builder, Assembly assembly) + where TDbContext : DbContext + { + // Configure repositories for all entities from the specified assembly + // read all entity classes from the assembly and register corresponding repositories + // the entity class must implement the IEntity interface + var entityTypes = assembly.GetTypes() + .Where(t => t.GetInterfaces().Any(i => i == typeof(IEntity))) + .ToList(); + + return builder.ConfigureRepositories(r => + { + foreach (var entityType in entityTypes) + { + r.Add(entityType); + } + }); + } +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/WorkContextServiceCollectionExtensions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Extensions/WorkContextServiceCollectionExtensions.cs similarity index 87% rename from RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/WorkContextServiceCollectionExtensions.cs rename to RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Extensions/WorkContextServiceCollectionExtensions.cs index 9a75c70..c12258b 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/WorkContextServiceCollectionExtensions.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Extensions/WorkContextServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using RoyalCode.SmartSearch.EntityFramework.Services; using RoyalCode.SmartSearch.Linq; using RoyalCode.UnitOfWork; +using RoyalCode.UnitOfWork.EntityFramework; using RoyalCode.WorkContext; using RoyalCode.WorkContext.Commands; using RoyalCode.WorkContext.EntityFramework; @@ -21,6 +22,23 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class WorkContextServiceCollectionExtensions { + /// + /// Adds a work context related to a default , + /// and configure the with services. + /// + /// The service collection. + /// The services lifetime, by default is scoped. + /// + /// A unit of work builder to configure the and services like repositories and searches. + /// + public static IWorkContextBuilder AddWorkContextDefault( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddWorkContextInternal, DefaultDbContext>(lifetime) + .ConfigureDbContextWithService(); + } + /// /// Adds a work context related to a . /// @@ -79,7 +97,7 @@ public static IWorkContextBuilder AddWorkContext(lifetime); } - private static WorkContextBuilder AddWorkContextInternal( + private static IWorkContextBuilder AddWorkContextInternal( this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Scoped) where TWorkContext : IWorkContext diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Querying/Configurations/IQueryConfigurerBase.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Querying/Configurations/IQueryConfigurerBase.cs index 2b6fe08..11fd6a5 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Querying/Configurations/IQueryConfigurerBase.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Querying/Configurations/IQueryConfigurerBase.cs @@ -8,7 +8,7 @@ namespace RoyalCode.WorkContext.EntityFramework.Querying.Configurations; /// Provides access to the service collection for query configuration. /// /// The type of the configurer. -public interface IQueryConfigurerBase +public interface IQueryConfigurerBase where TConfigurer : IQueryConfigurerBase { /// @@ -66,7 +66,7 @@ TConfigurer Configure() /// /// The type of the . /// The type of the configurer. -public interface IQueryConfigurer : IQueryConfigurerBase, IQueryHandlerConfigurer +public interface IQueryConfigurer : IQueryConfigurerBase, IQueryHandlerConfigurer where TDbContext : DbContext where TConfigurer : IQueryConfigurer { } @@ -75,6 +75,6 @@ public interface IQueryConfigurer : IQueryConfigure /// Provides methods to configure query handlers for a specific . /// /// The type of the . -public interface IQueryConfigurer : IQueryConfigurer> +public interface IQueryConfigurer : IQueryConfigurer> where TDbContext : DbContext { } \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Querying/Configurations/IQueryHandlerConfigurer.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Querying/Configurations/IQueryHandlerConfigurer.cs index 2491b0f..84e5e72 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Querying/Configurations/IQueryHandlerConfigurer.cs +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/Querying/Configurations/IQueryHandlerConfigurer.cs @@ -7,7 +7,7 @@ namespace RoyalCode.WorkContext.EntityFramework.Querying.Configurations; /// Configures query handlers for a specific . /// /// The type of the . -public interface IQueryHandlerConfigurer : IQueryHandlerConfigurer> +public interface IQueryHandlerConfigurer : IQueryHandlerConfigurer> where TDbContext : DbContext { } @@ -16,7 +16,7 @@ public interface IQueryHandlerConfigurer : IQueryHandlerConfigurer /// The type of the . /// The type of the configurer. -public interface IQueryHandlerConfigurer +public interface IQueryHandlerConfigurer where TDbContext : DbContext where TConfigure : IQueryHandlerConfigurer { diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/RoyalCode.WorkContext.EntityFramework.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/RoyalCode.WorkContext.EntityFramework.csproj index 983f14f..d47ba4d 100644 --- a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/RoyalCode.WorkContext.EntityFramework.csproj +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.EntityFramework/RoyalCode.WorkContext.EntityFramework.csproj @@ -25,7 +25,7 @@ - + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.PostgreSql/Extensions/PgWorkContextExtensions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.PostgreSql/Extensions/PgWorkContextExtensions.cs new file mode 100644 index 0000000..98e5a47 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.PostgreSql/Extensions/PgWorkContextExtensions.cs @@ -0,0 +1,78 @@ + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using RoyalCode.UnitOfWork.EntityFramework; +using RoyalCode.WorkContext.EntityFramework.Configurations; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extensions methods for to add PostgreSQL-based work context. +/// +public static class PgWorkContextExtensions +{ + /// + /// Adds a PostgreSQL-based work context for the to the service collection. + /// + /// The to which the work context will be added. + /// + /// The name of the connection string, used to get the connection string from . + /// + /// The services lifetime, by default is scoped. + /// + /// A unit of work builder to configure the and services like repositories and searches. + /// + public static IWorkContextBuilder AddPostgreWorkContextDefault( + this IServiceCollection services, + string connectionStringName = "Default", + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddPostgreWorkContext(connectionStringName, lifetime); + } + + /// + /// Adds a PostgreSQL-based work context related to a . + /// + /// The type of the DbContext used in the work context. + /// The service collection. + /// + /// The name of the connection string, used to get the connection string from . + /// + /// The services lifetime, by default is scoped. + /// + /// A unit of work builder to configure the and services like repositories and searches. + /// + public static IWorkContextBuilder AddPostgreWorkContext( + this IServiceCollection services, + string connectionStringName = "Default", + ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TDbContext : DbContext + { + return services.AddWorkContext(lifetime) + .ConfigureDbContextWithService() + .ConfigureOptions((provider, options) => + { + var configuration = provider.GetRequiredService(); + options.UseNpgsql(configuration.GetConnectionString(connectionStringName)); + }); + } + + /// + /// Allows you to configure specific Npgsql options for the Entity Framework context. + /// + /// The type of the DbContext used in the work context. + /// The work context builder to which the Npgsql options will be added. + /// The action to configure the Npgsql options. + /// The same builder for chained calls. + public static IWorkContextBuilder ConfigureNpgsqlOptions( + this IWorkContextBuilder builder, + Action configure) + where TDbContext : DbContext + { + return builder.ConfigureOptions((sp, ob) => + { + configure(new NpgsqlDbContextOptionsBuilder(ob)); + }); + } +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.PostgreSql/RoyalCode.WorkContext.PostgreSql.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.PostgreSql/RoyalCode.WorkContext.PostgreSql.csproj new file mode 100644 index 0000000..c9b4f1f --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.PostgreSql/RoyalCode.WorkContext.PostgreSql.csproj @@ -0,0 +1,29 @@ + + + + + + $(LibTargets) + $(PersistVer)$(PersistPreview) + $(PersistVer) + $(PersistVer) + + Persistence components implementation with EntityFrameworkCore and PostgreSql, + including the handling of unit of works, repositories and searches. + The Work Context is an pattern that include the Unit Of Work functionality + and add access to others patterns like Repository, Search, Event, Message Bus, etc. + + + RoyalCode Enterprise-Patterns UoW Unit-Of-Work WorkContext Work-Context + + + + + + + + + + + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/Extensions/SqlServerWorkContextExtensions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/Extensions/SqlServerWorkContextExtensions.cs new file mode 100644 index 0000000..406766b --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/Extensions/SqlServerWorkContextExtensions.cs @@ -0,0 +1,90 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Configuration; +using RoyalCode.UnitOfWork.EntityFramework; +using RoyalCode.WorkContext.EntityFramework.Configurations; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extensions methods for to add SQL Server-based work context. +/// +public static class SqlServerWorkContextExtensions +{ + /// + /// Adds a SQL Server-based work context for the to the service collection. + /// + /// The to which the work context will be added. + /// + /// The name of the connection string, used to get the connection string from . + /// + /// The services lifetime, by default is scoped. + /// + /// A unit of work builder to configure the and services like repositories and searches. + /// + public static IWorkContextBuilder AddSqlServerWorkContextDefault( + this IServiceCollection services, + string connectionStringName = "Default", + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddSqlServerWorkContext(connectionStringName, lifetime); + } + + /// + /// Adds a SQL Server-based work context related to a . + /// + /// The type of the DbContext used in the work context. + /// The service collection. + /// + /// The name of the connection string, used to get the connection string from . + /// + /// The services lifetime, by default is scoped. + /// + /// A unit of work builder to configure the and services like repositories and searches. + /// + public static IWorkContextBuilder AddSqlServerWorkContext( + this IServiceCollection services, + string connectionStringName = "Default", + ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TDbContext : DbContext + { + return services.AddWorkContext(lifetime) + .ConfigureDbContextWithService() + .ConfigureOptions((provider, options) => + { + var configuration = provider.GetRequiredService(); + options.UseSqlServer(configuration.GetConnectionString(connectionStringName)); + }); + } + + /// + /// Allows you to configure specific SqlServer options for the Entity Framework context. + /// + /// The work context builder to which the SqlServer options will be added. + /// The action to configure the SqlServer options. + /// The same builder for chained calls. + public static IWorkContextBuilder ConfigureSqlServerOptions( + this IWorkContextBuilder builder, + Action configure) + where TDbContext : DbContext + { + return builder.ConfigureOptions((sp, ob) => + { + configure(new SqlServerDbContextOptionsBuilder(ob)); + }); + } + + /// + /// Configures the context to use relational database semantics when comparing null + /// values. By default, Entity Framework will use C# semantics for null values, and + /// generate SQL to compensate for differences in how the database handles nulls. + /// + /// The work context builder to which the SqlServer options will be added. + /// The same builder for chained calls. + public static IWorkContextBuilder UseRelationalNulls( + this IWorkContextBuilder builder) + where TDbContext : DbContext + { + return builder.ConfigureSqlServerOptions(b => b.UseRelationalNulls()); + } +} diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/RoyalCode.WorkContext.SqlServer.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/RoyalCode.WorkContext.SqlServer.csproj new file mode 100644 index 0000000..1a0bc4b --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.SqlServer/RoyalCode.WorkContext.SqlServer.csproj @@ -0,0 +1,29 @@ + + + + + + $(LibTargets) + $(PersistVer)$(PersistPreview) + $(PersistVer) + $(PersistVer) + + Persistence components implementation with EntityFrameworkCore and SqlServer, + including the handling of unit of works, repositories and searches. + The Work Context is an pattern that include the Unit Of Work functionality + and add access to others patterns like Repository, Search, Event, Message Bus, etc. + + + RoyalCode Enterprise-Patterns UoW Unit-Of-Work WorkContext Work-Context + + + + + + + + + + + + diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/Extensions/SqliteWorkContextExtensions.cs b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/Extensions/SqliteWorkContextExtensions.cs new file mode 100644 index 0000000..604cbee --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/Extensions/SqliteWorkContextExtensions.cs @@ -0,0 +1,248 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Configuration; +using RoyalCode.UnitOfWork.EntityFramework; +using RoyalCode.WorkContext.EntityFramework.Configurations; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extensions methods for to add Sqlite-based work context. +/// +public static class SqliteWorkContextExtensions +{ + /// + /// Adds a Sqlite-based work context related to a default , + /// and configure the with services. + /// + /// The service collection. + /// + /// The name of the connection string, used to get the connection string from . + /// + /// The services lifetime, by default is scoped. + /// + /// A unit of work builder to configure the and services like repositories and searches. + /// + public static IWorkContextBuilder AddSqliteWorkContextDefault( + this IServiceCollection services, + string connectionStringName = "Default", + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddSqliteWorkContext(connectionStringName, lifetime); + } + + /// + /// Adds a Sqlite-based work context related to a . + /// + /// The type of the DbContext used in the work context. + /// The service collection. + /// + /// The name of the connection string, used to get the connection string from . + /// + /// The services lifetime, by default is scoped. + /// + /// A unit of work builder to configure the and services like repositories and searches. + /// + public static IWorkContextBuilder AddSqliteWorkContext( + this IServiceCollection services, + string connectionStringName = "Default", + ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TDbContext : DbContext + { + return services.AddWorkContext(lifetime) + .ConfigureDbContextWithService() + .ConfigureOptions((provider, options) => + { + var configuration = provider.GetRequiredService(); + options.UseSqlite(configuration.GetConnectionString(connectionStringName)); + }); + } + + /// + /// Adds a Sqlite-based work context related to a + /// and creates an in-memory database connection to configure the with services. + /// + /// The service collection. + /// An action to configure the connection, optional. + /// The services lifetime, by default is scoped. + /// + /// A unit of work builder to configure the and services like repositories and searches. + /// + public static IWorkContextBuilder AddSqliteInMemoryWorkContextDefault( + this IServiceCollection services, + ConfigureInMemorySqliteConnection? configureConnection = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + return services.AddSqliteInMemoryWorkContext(configureConnection, lifetime); + } + + /// + /// Adds a Sqlite-based work context related to a + /// and creates an in-memory database connection to configure the with services. + /// + /// The type of the DbContext used in the work context. + /// The service collection. + /// An action to configure the connection, optional. + /// The services lifetime, by default is scoped. + /// + /// A unit of work builder to configure the and services like repositories and searches. + /// + public static IWorkContextBuilder AddSqliteInMemoryWorkContext( + this IServiceCollection services, + ConfigureInMemorySqliteConnection? configureConnection = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TDbContext : DbContext + { + // creates a single SQLite connection in memory + var connection = new SqliteConnection("DataSource=:memory:"); + services.AddSingleton(connection); + + return services.AddWorkContext(lifetime) + .ConfigureDbContextWithService() + .ConfigureOptions((provider, options) => + { + var conn = provider.GetRequiredService(); + + if (conn.State != System.Data.ConnectionState.Open) + { + conn.Open(); + if (configureConnection is not null) + configureConnection(connection, provider); + + provider.GetService>() + ?.Configure(conn, provider); + } + + options.UseSqlite(conn); + }); + } + + /// + /// Allows you to configure specific Sqlite options for the Entity Framework context. + /// + /// The type of the DbContext used in the work context. + /// The work context builder to which the Sqlite options will be added. + /// The action to configure the Sqlite options. + /// The same builder for chained calls. + public static IWorkContextBuilder ConfigureSqliteOptions( + this IWorkContextBuilder builder, + Action configure) + where TDbContext : DbContext + { + return builder.ConfigureOptions((sp, ob) => + { + configure(new SqliteDbContextOptionsBuilder(ob)); + }); + } + + /// + /// Adds a configuration to ensure the database is created when the work context is built. + /// + /// The type of the DbContext used in the work context. + /// The work context builder to which the configuration will be added. + /// The same builder for chained calls. + public static IWorkContextBuilder EnsureDatabaseCreated( + this IWorkContextBuilder builder) + where TDbContext : DbContext + { + InternalInMemorySqliteConfigureConnection.GetFromServices(builder.Services) + .Configure((connection, sp) => + { + using var scope = sp.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); + return Task.CompletedTask; + }); + + return builder; + } + + /// + /// Adds a configuration to seed the database with initial data when the work context is built. + /// + /// The type of the DbContext used in the work context. + /// The work context builder to which the configuration will be added. + /// The action to seed the database, which receives the . + /// The same builder for chained calls. + public static IWorkContextBuilder SeedDatabase( + this IWorkContextBuilder builder, + Func seedAction) + where TDbContext : DbContext + { + InternalInMemorySqliteConfigureConnection.GetFromServices(builder.Services) + .Configure(async (connection, sp) => + { + using var scope = sp.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + await seedAction(context); + }); + + return builder; + } + + /// + /// Adds a configuration to seed the database with initial data when the work context is built, + /// + /// The type of the DbContext used in the work context. + /// The work context builder to which the configuration will be added. + /// The action to seed the database, which receives the and . + /// The same builder for chained calls. + public static IWorkContextBuilder SeedDatabase( + this IWorkContextBuilder builder, + Func seedAction) + where TDbContext : DbContext + { + InternalInMemorySqliteConfigureConnection.GetFromServices(builder.Services) + .Configure(async (connection, sp) => + { + using var scope = sp.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + await seedAction(context, scope.ServiceProvider); + }); + + return builder; + } +} + +/// +/// A delegate to configure an in-memory SQLite connection for a specific . +/// +/// The type of the DbContext. +/// The SQLite connection to configure. +/// The IServiceProvider to use for service resolution. +public delegate Task ConfigureInMemorySqliteConnection(SqliteConnection connection, IServiceProvider sp) + where TDb : DbContext; + +internal sealed class InternalInMemorySqliteConfigureConnection + where TDb : DbContext +{ + public static InternalInMemorySqliteConfigureConnection GetFromServices(IServiceCollection services) + { + var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(InternalInMemorySqliteConfigureConnection)); + if (descriptor is null || descriptor.ImplementationInstance is not InternalInMemorySqliteConfigureConnection options) + { + options = new InternalInMemorySqliteConfigureConnection(); + services.AddSingleton(options); + } + return options; + } + + private ConfigureInMemorySqliteConnection? configure; + + public void Configure(SqliteConnection connection, IServiceProvider sp) + { + if (configure is null) + return; + + configure(connection, sp).GetAwaiter().GetResult(); + } + + public void Configure(ConfigureInMemorySqliteConnection configure) + { + if (this.configure is null) + this.configure = configure; + else + this.configure += configure; + } +} \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/RoyalCode.WorkContext.Sqlite.csproj b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/RoyalCode.WorkContext.Sqlite.csproj new file mode 100644 index 0000000..8003087 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/RoyalCode.WorkContext.Sqlite/RoyalCode.WorkContext.Sqlite.csproj @@ -0,0 +1,29 @@ + + + + + + $(LibTargets) + $(PersistVer)$(PersistPreview) + $(PersistVer) + $(PersistVer) + + Persistence components implementation with EntityFrameworkCore and Sqlite, + including the handling of unit of works, repositories and searches. + The Work Context is an pattern that include the Unit Of Work functionality + and add access to others patterns like Repository, Search, Event, Message Bus, etc. + + + RoyalCode Enterprise-Patterns UoW Unit-Of-Work WorkContext Work-Context + + + + + + + + + + + + diff --git a/RoyalCode.EnterprisePatterns/base.targets b/RoyalCode.EnterprisePatterns/base.targets index def09f8..e20e26f 100644 --- a/RoyalCode.EnterprisePatterns/base.targets +++ b/RoyalCode.EnterprisePatterns/base.targets @@ -10,12 +10,12 @@ Royal Code Copyright © $([System.DateTime]::UtcNow.ToString("yyyy")) https://github.com/Royal-Code/EnterpisePatterns Git - icon.png + icon2.png README.md AGPL-3.0-only - + \ No newline at end of file diff --git a/RoyalCode.EnterprisePatterns/icon2.png b/RoyalCode.EnterprisePatterns/icon2.png new file mode 100644 index 0000000..b550414 Binary files /dev/null and b/RoyalCode.EnterprisePatterns/icon2.png differ diff --git a/RoyalCode.EnterprisePatterns/instructions-for-copilot.md b/RoyalCode.EnterprisePatterns/instructions-for-copilot.md new file mode 100644 index 0000000..3477c09 --- /dev/null +++ b/RoyalCode.EnterprisePatterns/instructions-for-copilot.md @@ -0,0 +1,57 @@ +# Instruções para GitHub Copilot – Uso de RoyalCode WorkContext, UnitOfWork e Repositories + +Estas regras ajudam o Copilot a gerar código correto usando as bibliotecas RoyalCode: + +## Contexto e preferências +- Use `RoyalCode.WorkContext` como API principal para persistência, repositórios, comandos e consultas. +- Evite acessar `DbContext` diretamente; prefira `IWorkContext`. +- Registre repositórios apenas para raízes de agregado quando aplicável. +- Para desenvolvimento/teste, use SQLite in‑memory; produção: configure Npgsql (PostgreSQL) ou SQL Server. + +## Setup padrão (DI + EF Core) +- Configure WorkContext com o provider correto: + - Sqlite in‑memory: `UseSqlite(new SqliteConnection("DataSource=:memory:"))` + `conn.Open()`. + - Sqlite arquivo: `UseSqlite("Data Source=mydb.sqlite")`. + - PostgreSQL: `UseNpgsql("Host=...;Database=...;Username=...;Password=...")`. + - SQL Server: `UseSqlServer("Server=...;Database=...;Integrated Security=True")`. +- Após configurar, chame `EnsureDatabaseCreated()` apenas em dev/test. +- Use `ConfigureModel(...)` e/ou `ConfigureMappingsFromAssembly(...)` para aplicar mapeamentos. +- Registre componentes por assembly: + - `AddRepositories(assembly)` + - `ConfigureSearches(assembly)` + - `ConfigureCommands(assembly)` + - `ConfigureQueries(assembly)` + +## Operações de domínio +- Entidade: herde de `Entity`. Ex.: `public class Person : Entity { /* ... */ }`. +- Agregado: herde de `AggregateRoot` e dispare eventos com `AddEvent(...)`. + +## Operações de persistência +- Repositório: `var repo = context.Repository();` +- Criar e salvar: `await repo.AddAsync(entity); await context.SaveAsync();` +- Buscar por ID: `await repo.FindAsync(id);` +- Criteria: `var criteria = context.Criteria(); var list = criteria.Collect();` + +## Commands +- Defina requests/handlers: + - `class CreatePerson : ICommandRequest { public string Name { get; set; } }` + - `class CreatePersonHandler : ICommandHandler { /* chama context.Add e SaveAsync */ }` +- Envie: `await context.SendAsync(new CreatePerson { Name = "John" }, ct);` +- Se usar dispatcher: registre `AddCommandDispatcher()`. + +## Queries +- Lista: `IQueryRequest` e handler com `ToListAsync`. +- Stream: `IAsyncQueryRequest` e handler com `AsAsyncEnumerable`. +- Execução: `await context.QueryAsync(request, ct)` ou `await foreach (var item in context.QueryAsync(request, ct)) { }`. + +## Boas práticas e anti‑padrões +- Centralize regras de escrita em Commands; consultas em Queries. +- Não salve fora de `IWorkContext.SaveAsync()`. +- Não acesse `DbContext` diretamente em serviços de aplicação. +- Mantenha mapeamentos e registros por assembly para modularidade. + +## Troubleshooting +- Handler não registrado: verifique `ConfigureCommands/ConfigureQueries` e o assembly correto. +- Banco não criado em dev/test: chame `EnsureDatabaseCreated()`. +- Conexão SQLite in‑memory: mantenha a conexão aberta durante o uso do contexto. +- DI: confirme `AddWorkContext()` e construa o provedor após configurações. diff --git a/RoyalCode.EnterprisePatterns/tests.targets b/RoyalCode.EnterprisePatterns/tests.targets index c9ceb28..946c1ef 100644 --- a/RoyalCode.EnterprisePatterns/tests.targets +++ b/RoyalCode.EnterprisePatterns/tests.targets @@ -1,24 +1,24 @@ - net9 + net10.0 enable enable false true - 9.0.5 + 10.0.2 - + - + - - + + \ No newline at end of file diff --git a/RoyalCode.Examples/Directory.Build.props b/RoyalCode.Examples/Directory.Build.props index 3f295d5..74139a6 100644 --- a/RoyalCode.Examples/Directory.Build.props +++ b/RoyalCode.Examples/Directory.Build.props @@ -1,7 +1,12 @@ 9.0.0 - 1.0.0-preview-8.9 + 0.8.5 + 0.8.2 + 1.0.0-preview-4.4 + 0.0.7 + 0.1.0 9.0.7 + 2.6.3 diff --git a/RoyalCode.Examples/RoyalCode.Examples.Api/EF/AppDbContext.cs b/RoyalCode.Examples/RoyalCode.Examples.Api/EF/AppDbContext.cs new file mode 100644 index 0000000..df744b0 --- /dev/null +++ b/RoyalCode.Examples/RoyalCode.Examples.Api/EF/AppDbContext.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore; + +namespace RoyalCode.Examples.Api.EF; + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) + { + } +} diff --git a/RoyalCode.Examples/RoyalCode.Examples.Api/Program.cs b/RoyalCode.Examples/RoyalCode.Examples.Api/Program.cs index c040e1d..e24f4b9 100644 --- a/RoyalCode.Examples/RoyalCode.Examples.Api/Program.cs +++ b/RoyalCode.Examples/RoyalCode.Examples.Api/Program.cs @@ -1,4 +1,6 @@ using RoyalCode.Examples.Blogs.Api; +using RoyalCode.Examples.Blogs.Infra.Persistence; +using RoyalCode.SmartCommands.WorkContext.Extensions; using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); @@ -6,6 +8,12 @@ builder.Services.AddOpenApi(); builder.Services.AddBlobs(); +builder.Services + .AddSqliteInMemoryWorkContextDefault() + .EnsureDatabaseCreated() + .ConfigureBlogs() + .AddUnitOfWorkAccessor(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -19,28 +27,4 @@ app.MapBlob(); -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast"); - app.Run(); - -internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} diff --git a/RoyalCode.Examples/RoyalCode.Examples.Api/RoyalCode.Examples.Api.csproj b/RoyalCode.Examples/RoyalCode.Examples.Api/RoyalCode.Examples.Api.csproj index 44d7b3f..bd4b59a 100644 --- a/RoyalCode.Examples/RoyalCode.Examples.Api/RoyalCode.Examples.Api.csproj +++ b/RoyalCode.Examples/RoyalCode.Examples.Api/RoyalCode.Examples.Api.csproj @@ -1,18 +1,19 @@ - - net9.0 - enable - enable - + + net9.0 + enable + enable + - - - - + + + + + - - - + + + diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Api/BlogsEndpoints.cs b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Api/BlogsEndpoints.cs index 15e3018..e327eb9 100644 --- a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Api/BlogsEndpoints.cs +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Api/BlogsEndpoints.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using RoyalCode.Examples.Blogs.Contracts.Authors; +using RoyalCode.Examples.Blogs.Core.Support; using RoyalCode.SmartCommands; namespace RoyalCode.Examples.Blogs.Api; @@ -8,6 +12,16 @@ public static partial class BlogsEndpoints { public static void MapBlob(this IEndpointRouteBuilder app) { - app.MapHelloGroup(); + app.MapHelloGroup().WithTags("Hello"); + + var authors = app.MapAuthorGroup().WithTags("Author").WithExceptionFilter(); + + authors.MapSearch("") + .WithSummary("Search Authors") + .WithDescription("Searches for authors based on the provided filter criteria.") + .WithName("author-search") + .WithOpenApi(); + + } } diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/AuthorDetails.cs b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/AuthorDetails.cs new file mode 100644 index 0000000..482f25c --- /dev/null +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/AuthorDetails.cs @@ -0,0 +1,34 @@ +using RoyalCode.Examples.Blogs.Core.Support; +using RoyalCode.SmartCommands; + +namespace RoyalCode.Examples.Blogs.Contracts.Authors; + +/// +/// A class representing the details of an author. +/// +[MapGroup("author")] +[MapFind("{id}", "find-author-by-id"), EntityReference] +[WithSummary("Author Details")] +[WithDescription("Represents the details of an author, including their unique identifier, name, email, and creation date.")] +public class AuthorDetails +{ + /// + /// The unique identifier of the author. + /// + public Guid Id { get; set; } + + /// + /// The name of the author. + /// + public string? Name { get; set; } + + /// + /// The email of the author. + /// + public string? Email { get; set; } + + /// + /// The date and time when the author was created. + /// + public DateTime CreatedAt { get; set; } +} diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/AuthorEmailVerificationDetails.cs b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/AuthorEmailVerificationDetails.cs new file mode 100644 index 0000000..993182b --- /dev/null +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/AuthorEmailVerificationDetails.cs @@ -0,0 +1,56 @@ +using RoyalCode.Examples.Blogs.Core.Support; +using RoyalCode.SmartCommands; +using RoyalCode.SmartProblems; +using RoyalCode.SmartProblems.Entities; +using RoyalCode.SmartSelector; +using RoyalCode.WorkContext; + +namespace RoyalCode.Examples.Blogs.Contracts.Authors; + +[AutoSelect] +public partial class AuthorEmailVerificationDetails +{ + public string AuthorName { get; set; } + + public string LinkCode { get; set; } + + public string UICode { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public DateTimeOffset ValidUntil { get; set; } + + public DateTimeOffset? ValidatedAt { get; set; } + + [WithUnitOfWork] + public static IEnumerable Query([WithParameter] Author author) + { + return author.EmailVerifications.SelectAuthorEmailVerificationDetails(); + } +} + +public interface IAuthorEmailVerificationDetailsQueryHandler +{ + Task>> HandleAsync(Id id, CancellationToken ct); +} + +public class AuthorEmailVerificationDetailsQueryHandler : IAuthorEmailVerificationDetailsQueryHandler +{ + private readonly IUnitOfWorkAccessor accessor; + + public AuthorEmailVerificationDetailsQueryHandler(IUnitOfWorkAccessor accessor) + { + this.accessor = accessor; + } + + public async Task>> HandleAsync(Id id, CancellationToken ct) + { + var findResult = await accessor.FindEntityAsync(id, ct); + if (findResult.NotFound(out var notFoundProblem)) + return notFoundProblem; + + var result = AuthorEmailVerificationDetails.Query(findResult.Entity); + + return new(result); + } +} \ No newline at end of file diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/AuthorFilter.cs b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/AuthorFilter.cs new file mode 100644 index 0000000..d8c87d1 --- /dev/null +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/AuthorFilter.cs @@ -0,0 +1,17 @@ +namespace RoyalCode.Examples.Blogs.Contracts.Authors; + +public class AuthorFilter +{ + /// + /// The name of the author to filter by. + /// + public string? Name { get; set; } + /// + /// The email of the author to filter by. + /// + /// + public string? Email { get; set; } + /// + /// The page number for pagination. + /// +} diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/RegisterAuthor.cs b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/RegisterAuthor.cs index 450e097..179ae8e 100644 --- a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/RegisterAuthor.cs +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/RegisterAuthor.cs @@ -2,16 +2,15 @@ using RoyalCode.SmartCommands; using RoyalCode.SmartProblems; using RoyalCode.SmartValidations; -using RoyalCode.WorkContext.Abstractions; -using System.ComponentModel; +using RoyalCode.WorkContext; using System.Diagnostics.CodeAnalysis; namespace RoyalCode.Examples.Blogs.Contracts.Authors; -[MapGroup("authors")] -[MapPost("", "register-author")] -[Description("Registers a new author in the system.")] -[MapIdResultValue] +[MapGroup("author")] +[MapPost("", "author-register"), MapCreatedRoute("{0}", "Id"), MapIdResultValue] +[WithSummary("Register Author")] +[WithDescription("Registers a new author in the system.")] public partial class RegisterAuthor : IValidable { /// diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/VerifyEmail.cs b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/VerifyEmail.cs new file mode 100644 index 0000000..0c9860d --- /dev/null +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Contracts/Authors/VerifyEmail.cs @@ -0,0 +1,51 @@ +using RoyalCode.Examples.Blogs.Core.Support; +using RoyalCode.SmartCommands; +using RoyalCode.SmartProblems; +using RoyalCode.SmartValidations; +using RoyalCode.WorkContext; +using System.Diagnostics.CodeAnalysis; + +namespace RoyalCode.Examples.Blogs.Contracts.Authors; + +[MapGroup("author")] +[MapPost("verify-email", "author-verify-email")] +[WithSummary("Verify Email")] +[WithDescription("Verifies the email of the author using the verification code sent via email.")] +public partial class VerifyEmail : IValidable +{ + /// + /// Code to verify the email sent to the user via email link. + /// + public string? LinkCode { get; set; } + + /// + /// Code to verify the email informed by the user in the UI. + /// + public string? UICode { get; set; } + + /// + /// Validates the email verification codes. + /// + /// Returns problems if any validation fails. + /// True if has problems, false otherwise (valid). + [MemberNotNullWhen(false, nameof(LinkCode), nameof(UICode))] + public bool HasProblems([NotNullWhen(true)] out Problems? problems) + { + return Rules.Set() + .NotNull(LinkCode) + .NotNull(UICode) + .HasProblems(out problems); + } + + [Command, WithValidateModel, WithUnitOfWork] + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InvalidState)] + internal async Task Execute(IWorkContext context) + { + WasValidated(); + return await context.Repository().FindAsync(e => e.LinkCode == LinkCode) + .ContinueAsync(emailVerification => + { + return emailVerification.TryValidate(UICode); + }); + } +} diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Core/Support/Author.cs b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Core/Support/Author.cs index 5cad49e..08b3b3d 100644 --- a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Core/Support/Author.cs +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Core/Support/Author.cs @@ -9,7 +9,8 @@ public Author(string name, string email) Id = Guid.CreateVersion7(); Name = name; Email = email; - CreatedDate = DateTime.UtcNow; + CreatedAt = DateTime.UtcNow; + EmailVerifications = [new EmailVerification(this)]; } #nullable disable @@ -25,10 +26,12 @@ public Author() { } public bool IsConfirmed { get; set; } = false; - public DateTime CreatedDate { get; set; } + public DateTime CreatedAt { get; set; } public DateTime? LastModifiedDate { get; set; } + public ICollection EmailVerifications { get; set; } + public void ConfirmAuthor() { IsConfirmed = true; diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Core/Support/EmailVerification.cs b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Core/Support/EmailVerification.cs new file mode 100644 index 0000000..141986f --- /dev/null +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Core/Support/EmailVerification.cs @@ -0,0 +1,67 @@ +using RoyalCode.Entities; +using RoyalCode.SmartProblems; +using System.Text; + +namespace RoyalCode.Examples.Blogs.Core.Support; + +public class EmailVerification : Entity +{ + public EmailVerification(Author author) + { + Id = Guid.CreateVersion7(); + LinkCode = Guid.NewGuid().ToString("N"); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 6; i++) + { + sb.Append(Random.Shared.Next(0, 10)); + } + UICode = sb.ToString(); + + Author = author; + CreatedAt = DateTimeOffset.UtcNow; + ValidUntil = CreatedAt.AddDays(1); + } + +#nullable disable + /// + /// Constructor for deserialization purposes. + /// + public EmailVerification() { } +#nullable enable + + public string LinkCode { get; set; } + + public string UICode { get; set; } + + public Author Author { get; set; } + + public DateTimeOffset CreatedAt { get; set; } + + public DateTimeOffset ValidUntil { get; set; } + + public DateTimeOffset? ValidatedAt { get; set; } + + public Result TryValidate(string code) + { + if (ValidatedAt.HasValue) + { + return Problems.InvalidState("Email already validated."); + } + + if (UICode != code) + { + return Problems.InvalidParameter("Invalid verification code."); + } + + if (DateTimeOffset.UtcNow > ValidUntil) + { + return Problems.InvalidState("Verification code expired."); + } + + ValidatedAt = DateTimeOffset.UtcNow; + Author.ConfirmAuthor(); + + return Result.Ok(); + } +} diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Infra/Persistence/ConfigureWorkContext.cs b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Infra/Persistence/ConfigureWorkContext.cs index 12eb23b..6bddc6c 100644 --- a/RoyalCode.Examples/RoyalCode.Examples.Blogs/Infra/Persistence/ConfigureWorkContext.cs +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/Infra/Persistence/ConfigureWorkContext.cs @@ -1,13 +1,106 @@ -using RoyalCode.UnitOfWork.EntityFramework; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using RoyalCode.Examples.Blogs.Core.Blogs; +using RoyalCode.Examples.Blogs.Core.Support; +using RoyalCode.WorkContext.EntityFramework.Configurations; namespace RoyalCode.Examples.Blogs.Infra.Persistence; public static class ConfigureWorkContext { - - public static IUnitOfWorkBuilder ConfigureBlogs(this IUnitOfWorkBuilder builder) + public static IWorkContextBuilder ConfigureBlogs(this IWorkContextBuilder builder) + where TDbContext : DbContext { + builder + .ConfigureMappingsFromAssembly(typeof(ConfigureWorkContext).Assembly, addRepositories: true) + .ConfigureSearches(b => + { + b.Add(); + }); return builder; } } + +public class BlogMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasMany(b => b.Posts) + .WithOne(p => p.Blog) + .HasForeignKey("BlogId") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasOne(b => b.Owner) + .WithMany() + .HasForeignKey("OwnerId"); + + builder + .HasMany(b => b.Authors) + .WithMany(); + } +} + +public class PostMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasMany(p => p.Threads) + .WithOne() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasOne(p => p.Author) + .WithMany() + .HasForeignKey("AuthorId"); + + } +} + +public class ThreadMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasMany(t => t.Comments) + .WithOne() + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.Cascade); + } +} + +public class CommentMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasOne(c => c.Author) + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade); + } +} + +public class AuthorMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + + } +} + +public class EmailVerificationMapping : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasOne(ev => ev.Author) + .WithMany(a => a.EmailVerifications) + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade); + } +} \ No newline at end of file diff --git a/RoyalCode.Examples/RoyalCode.Examples.Blogs/RoyalCode.Examples.Blogs.csproj b/RoyalCode.Examples/RoyalCode.Examples.Blogs/RoyalCode.Examples.Blogs.csproj index 2bc6ad9..1c6f16d 100644 --- a/RoyalCode.Examples/RoyalCode.Examples.Blogs/RoyalCode.Examples.Blogs.csproj +++ b/RoyalCode.Examples/RoyalCode.Examples.Blogs/RoyalCode.Examples.Blogs.csproj @@ -7,12 +7,14 @@ - - - - + + + + + + - +