From 5bc29b914752ae6b576b1422fdec57bd30f0ef83 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 12:39:40 +0000 Subject: [PATCH 01/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20add=20deep=20repos?= =?UTF-8?q?itory=20improvement=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deep analysis of the EntityFramework package and the eQuantic.Core.Data contracts, covering security (SQL injection in SqlExecutor), correctness bugs (UnitOfWork double-dispose, MongoDb wrong-database, EXEC vs CALL), packaging/versioning of the parallel version lines, ~2.4k lines of provider duplication, the contract surface explosion, and CI/testing gaps. Includes a 5-phase execution plan and the breaking changes that would require a v5.0.0 of the contracts. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- docs/IMPROVEMENT_PLAN.md | 227 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/IMPROVEMENT_PLAN.md diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..d448dd7 --- /dev/null +++ b/docs/IMPROVEMENT_PLAN.md @@ -0,0 +1,227 @@ +# Plano de Melhoria — eQuantic.Core.Data.EntityFramework + +> Análise profunda realizada em 2026-07-16 sobre este repositório (v4.4.2 / linhas 6.x–10.x publicadas) +> e sobre o repositório de contratos [`eQuantic/core-data`](https://github.com/eQuantic/core-data) (v4.3.2). +> Todos os achados citam `arquivo:linha` e foram verificados no código-fonte, não inferidos. + +## Sumário executivo + +O pacote funciona e está publicado no nuget.org há anos (106 versões do pacote principal, 57 do +contrato), mas acumulou dívida em cinco camadas. Em ordem de gravidade: + +1. **Segurança (crítico):** o `SqlExecutor` monta SQL interpolando valores sem escape — injeção de SQL + real em `ExecuteFunction`/`ExecuteProcedure` nos providers SqlServer, PostgreSql e MySql. Além disso, + o CI publica no nuget.org a cada push em qualquer branch. +2. **Correção (crítico/alto):** disposal duplo do `UnitOfWork`, provider MongoDb operando no banco + errado (grava/apaga em silêncio), `EXEC` (T-SQL) copiado para PostgreSQL/MySQL, parâmetros perdidos no + `FromSqlRaw`, `configuration` descartado em `All`/`Any`, registro de DI que quebra em runtime para o + MongoDb, e updates em massa que gravam valores errados sem erro. +3. **Packaging/versionamento (crítico para consumidores):** o mesmo `PackageId` é publicado em linhas de + versão paralelas (4.x multi-target + 6.x/7.x/8.x/9.x/10.x single-target). O NuGet trata tudo como uma + linha do tempo única: "latest" = 10.0.2 (net10-only), quebrando o restore de quem está em net6–net9 e + fazendo a linha 4.x (a mais completa) parecer abandonada. São 21 csproj mantidos à mão, e o esquema já + produziu bugs reais de grafo de dependência. +4. **Duplicação estrutural:** ~2.400 linhas são cópias idênticas entre providers — PostgreSql e MySql são + 100% cópias renomeadas do SqlServer. É a causa-raiz do bug `EXEC`→`CALL` (copiado sem adaptar). +5. **Contratos (`eQuantic.Core.Data`):** explosão combinatória de interfaces + (`IAsyncReadRepository` tem **100 membros**; só `SumAsync` são 30 overloads), + NRT desabilitado, e a paginação retorna `IEnumerable` sem total de registros. + +O plano abaixo está em **5 fases**. As Fases 0–2 **não quebram contrato** e podem sair na linha atual. +As Fases 3–4 definem a **v5.0.0 dos contratos** (breaking deliberado) e a estratégia de versionamento +no nuget.org. + +--- + +## Parte I — Diagnóstico + +### 1. Segurança + +| # | Sev. | Problema | Local | +|---|------|----------|-------| +| S1 | 🔴 Crítico | **Injeção de SQL**: `GetQueryParameters` interpola valores com `string.Format(" '{0}'", value)` sem escapar aspas simples; `string`/`Guid`/`DateTime` entram crus no texto SQL, que vai para `FromSqlRaw`/`ExecuteSqlRaw` **sem `DbParameter`**. O `name` da função/procedure também é interpolado. Arquivo idêntico nos 3 providers SQL. | `SqlServer/Repository/SqlExecutor.cs:367,375-407` (idem PostgreSql e MySql) | +| S2 | 🟡 Médio | Chave NuGet exposta a qualquer push: workflow publica com `secrets.nuget_key` em push de **qualquer branch**, sem environment protegido nem gate de tag/release. | `.github/workflows/dotnetcore.yml:3,66-67` | + +**Correção do S1:** gerar placeholders (`@p0`/`$1`/`?`) e passar `DbParameter`s reais — a infraestrutura já +existe no próprio arquivo (`SetCommand`, `SqlExecutor.cs:419-445`) e é simplesmente ignorada nesses métodos. + +### 2. Bugs de correção — Core (`eQuantic.Core.Data.EntityFramework`) + +| # | Sev. | Problema | Local | +|---|------|----------|-------| +| C1 | 🔴 | **Double-dispose do UnitOfWork**: `AsyncQueryableRepository.Dispose(bool)` chama `base.Dispose()` (que já dispõe o UoW) e dispõe o UoW de novo — causado por campo `_disposed` sombreado na derivada. | `Repository/AsyncQueryableRepository.cs:756-773` + `Repository/QueryableRepository.cs:363-378` | +| C2 | 🔴 | **Ownership invertido do UoW**: repositórios dispõem o UnitOfWork **injetado**; com `AddGenericRepositories` o container também o dispõe → DbContext morto para os demais repositórios do escopo. | `Repository/QueryableRepository.cs:374`; `Read/QueryableReadRepository.cs:22`; `Write/WriteRepository.cs:15` | +| C3 | 🔴 | **Registro de DI quebra em runtime**: `ISqlUnitOfWork` é registrado incondicionalmente mesmo quando a implementação não o implementa (MongoDb) → `InvalidCastException` ao resolver. A linha 75 ainda duplica o registro de `IQueryableUnitOfWork` (código morto). | `Repository/Extensions/ServiceCollectionExtensions.cs:74-75` | +| A1 | 🟠 | **`All`/`Any` (sync) descartam `configuration`**: `return this.All(specification.SatisfiedBy());` ignora includes/no-tracking/sorting. As variantes async fazem certo — prova de que é bug, não design. | `Read/QueryableReadRepository.cs:208,233` | +| A2 | 🟠 | **`Get(id)` rejeita chaves default válidas com exceção errada**: `if (Equals(id, default(TKey))) throw new ArgumentNullException` — `Get(0)`/`Guid.Empty` lançam sobre um argumento não-nulo. | `Read/QueryableReadRepository.cs:254-257`; `Read/AsyncQueryableReadRepository.cs:357-360` | +| A3 | 🟠 | **Sync deferred vs async materializado**: `GetAll`/`GetFiltered`/`GetPaged` sync devolvem `IQueryable` viva disfarçada de `IEnumerable` (dupla enumeração = 2 queries; `ObjectDisposedException` tardia), enquanto os async fazem `ToListAsync`. Mesmo método, semânticas divergentes. | `Read/QueryableReadRepository.cs:47,271,299,396` vs `Read/AsyncQueryableReadRepository.cs:44,336,732` | +| A4 | 🟠 | **Chave como `Expression.Constant`**: `GetFindByKeyExpression` embute o valor da chave na árvore → EF não parametriza; cada id gera entrada nova no cache de queries e SQL com literal (poluição do plan cache). | `Repository/Extensions/DbContextExtensions.cs:25,43` | +| A5 | 🟠 | **Paginação sem `OrderBy`**: `Skip/Take` sem ordenação garantida → páginas não determinísticas + warning `RowLimitingOperationWithoutOrderBy` do EF. | `Read/QueryableReadRepository.cs:395`; `Read/AsyncQueryableReadRepository.cs:729` | +| M-core | 🟡 | Vários: NRT desabilitado no pacote inteiro; **zero `ConfigureAwait(false)`** em toda a biblioteca; `SumAsync` (≈24 overloads) sem `CancellationToken` nem validação de null; `AddRepository` ignora o lifetime configurado; reflection sem cache no caminho quente de `Get(id, config)`; `GetAllAsync` injeta `Where(_ => true)` redundante; tracking inconsistente entre `Get(id)` (usa `Find`) e `Get(id, config)` (usa query). | ver relatório detalhado por arquivo | + +### 3. Bugs de correção — Providers + +| # | Sev. | Problema | Local | +|---|------|----------|-------| +| P1 | 🔴 | **MongoDb opera no banco errado**: `GetDatabase(_collectionName)` usa o nome da **coleção** como nome do **banco** → `DeleteMany`/`UpdateMany` executam contra banco inexistente e retornam `0` **silenciosamente**. | `MongoDb/Repository/Set.cs:129-131` | +| P2 | 🔴 | **MongoDb `UpdateDefinitionBuilder` grava constante**: `x => new E { Count = x.Count + 1 }` é compilado e invocado contra `Activator.CreateInstance(...)` (instância default) → grava `0+1=1` em todos os documentos, em vez de incrementar. Corrupção silenciosa. | `MongoDb/UpdateDefinitionBuilder.cs:38-43` | +| P3 | 🟠 | **`EXEC` (T-SQL) copiado para PG/MySQL**: `GetQueryProcedure` retorna `$"EXEC {name}..."`; PostgreSQL/MySQL exigem `CALL` → `ExecuteProcedure` falha em runtime. Evidência direta do copy-paste. | `PostgreSql/Repository/SqlExecutor.cs:367`; `MySql/…:367` | +| P4 | 🟠 | **`FromSqlRaw` recebe `IEnumerable` como 1 parâmetro**: `FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value))` — o `Select` vira um único elemento do `params object[]`. Falta `.ToArray()`. | `SqlExecutor.cs:219` (3 providers SQL) | +| P5 | 🟠 | **`ExpressionConverter` frágil**: `RewriteBinding` não faz rebind do parâmetro → updates que referenciam a entidade lançam em runtime; `GetMethods().…Single(...)` quebra se o EF adicionar overload de `SetProperty`; sem cache. | `SqlServer/ExpressionConverter.cs:50-68,137-155` (3 providers SQL) | +| P6 | 🟡 | **MongoDb: retornos silenciosos**: sem `IMongoClient` no DI, `GetCollection()` retorna null e os bulk ops retornam `0` sem lançar. `IsSimpleType` não cobre `Guid`/`DateTimeOffset`/coleções → updates descartados ou `TargetParameterCountException`. | `MongoDb/Repository/Set.cs:29-65`; `UpdateDefinitionBuilder.cs:45-73` | +| P7 | 🟡 | **`SqlExecutor.Dispose` destrói o `DbContext` injetado** (double-dispose no escopo DI). `IsMigrating` é `static` compartilhado entre todos os contextos do processo. Loop `do/while` de retry sem limite em `CommitAndRefreshChanges`. | `SqlExecutor.cs:483-497`; `UnitOfWork.cs:17,43-91` | +| P8 | 🟡 | **`GetEntityByIdSpecification` órfão**: existe só no SqlServer, nada nele é específico de SQL Server (delega para `DbContextExtensions` do base), zero usos no repo. Deveria estar no base ou ser deprecado. | `SqlServer/Specifications/GetEntityByIdSpecification.cs` | + +### 4. Duplicação estrutural + +| Arquivo | SqlServer | PostgreSql | MySql | MongoDb | +|---|---|---|---|---| +| `SqlExecutor.cs` | 498 linhas | **idêntico** | **idêntico** | — | +| `UnitOfWork.cs` | 266 | **idêntico** | **idêntico** | ~180 iguais | +| `ExpressionConverter.cs` | 175 | **idêntico** | **idêntico** | — | +| `Set.cs` | 287 | 268 (= SqlServer sem blocos legados) | **idêntico ao PG** | ~50 iguais | +| `SqlConfigurationExtensions.cs` | 14 | **idêntico** | **idêntico** | — | + +**~2.400–2.500 linhas redundantes.** As únicas diferenças genuínas de dialeto em `SqlExecutor` são 2 linhas +(`GetQueryFunction`/`GetQueryProcedure`). Não é dívida só estética: o bug P3 (`EXEC`→`CALL`) existe +justamente porque o arquivo foi copiado sem adaptar o dialeto. + +### 5. Packaging e versionamento + +| # | Sev. | Problema | Local | +|---|------|----------|-------| +| PK1 | 🔴 | **Linhas de versão paralelas no mesmo `PackageId`**: 4.x multi-target + 6.x/7.x/8.x/9.x/10.x single-target. O NuGet vê uma linha do tempo única → `dotnet add package` puxa 10.0.2 (net10-only) e quebra restore em net6–net9; Dependabot sugere upgrade impossível; major deixa de significar breaking (significa TFM). | 21 csproj em `src/`; confirmado no nuget.org | +| PK2 | 🔴 | **Bug de grafo de dependência**: `MySql.Net10.csproj` referencia o core **Net9** → o pacote MySql 10.0.2 declara dependência do core `>= 9.1.2` (net9-only). SqlServer/PG/MongoDb NetX referenciam o core multi-target 4.4.2 — famílias já cruzadas. | `MySql/…MySql.Net10.csproj:66-67` | +| PK3 | 🔴 | **Pomelo 9 sobre EF Core 10**: MySql net10.0 usa `Pomelo.EntityFrameworkCore.MySql 9.0.0` (compilado p/ EF 9) com `Microsoft.EntityFrameworkCore 10.0.3`. Não há Pomelo 10 estável — risco de incompatibilidade binária. | `MySql/…MySql.csproj` (bloco net10) | +| PK4 | 🟠 | **Diretórios `obj/`/`bin/` compartilhados**: vários csproj na mesma pasta sem `BaseIntermediateOutputPath` → `project.assets.json` sobrescrito a cada restore; builds paralelos (`dotnet build -m` da solution) são corrida declarada. | `src/*/` com múltiplos csproj | +| PK5 | 🟠 | **MongoDb principal desalinhado**: `Version 8.1.2.0`, só `net8.0` — não há MongoDb na família 4.x nem multi-target; consumidor net9/net10 recebe o build net8. | `MongoDb/…MongoDb.csproj:7,9` | +| PK6 | 🟠 | **`AssemblyVersion` rotativa** (muda a cada patch) → num diamante entre providers compilados contra linhas diferentes do core, risco de `MissingMethodException`/`FileLoadException`. | todos os csproj `:27` | +| PK7 | 🟡 | **MSBump morto e quebrado**: `build/MSBump.props` importa a si mesmo (circular), `MSBump.targets` chama `BumpVersion` sem `UsingTask`, `Directory.Build.targets` está em `build/` (não é ancestral de `src/`, nunca é aplicado) e o próprio comentário diz que é obsoleto desde NuGet 4.6. Nada disso é importado hoje. Quando funcionava, gerava versões não determinísticas por build. | `build/*` | + +### 6. Contratos (`eQuantic.Core.Data`) + +| # | Sev. | Problema | Local | +|---|------|----------|-------| +| K1 | 🟠 | **Explosão combinatória**: `IAsyncReadRepository` = **100 membros**; `IReadRepository` = 56; `ISqlUnitOfWork` ≈ 47 (inviável implementar à mão — anula o propósito de um contrato). `GetPagedAsync` = 18 overloads; `SumAsync` = 30. O commit mais recente *adicionou* 60 overloads de Sum — a tendência é piorar. | `Read/IAsyncReadRepository.cs`; `Sql/ISqlUnitOfWork.cs` | +| K2 | 🟠 | **`TUnitOfWork` como type parameter** habilita um único membro (`UnitOfWork { get; }`) mas contamina toda a hierarquia (~24 interfaces para 1 conceito) e cria acoplamento circular UoW↔repositório. Variância inconsistente sync vs async. | `Repository/IRepository.cs:26,55`; `IAsyncRepository.cs:39` | +| K3 | 🟠 | **Paginação sem metadados**: `GetPaged*` retorna `IEnumerable` cru, sem total/página → consumidor faz `Count()` separado (2 round-trips não atômicas). Falta um `PagedResult`. | `Read/IReadRepository.cs:267-314` | +| K4 | 🟡 | **Vazamento de EF no contrato**: `ISqlUnitOfWork` declara `GetPendingMigrations`/`UpdateDatabase`/`Attach` — contradiz a "persistence ignorance" que os próprios XML docs reivindicam. `IdentityGenerator` e `MigrationAttribute` são implementação num pacote de contratos. | `Sql/ISqlUnitOfWork.cs:23-127`; `IdentityGenerator.cs`; `Migration/MigrationAttribute.cs` | +| K5 | 🟡 | **`CancellationToken` ausente** em 30 `SumAsync`, `AddAsync`/`MergeAsync`/`ModifyAsync`/`RemoveAsync`, `LoadCollectionAsync`. NRT desabilitado (`Get*` retorna `Task` sem anotar null). Sem `IAsyncEnumerable`/streaming. | `Read/IAsyncReadRepository.cs`; `Write/IAsyncWriteRepository.cs` | +| K6 | 🟡 | **Constraint `new()` em tudo** (hostil a DDD) e `IEntity`/`IEntity` não ligam o `TKey` do repositório à chave real da entidade (`IRepository` compila mesmo se a chave for `int`). Deveria ser `where TEntity : IEntity`. | `Repository/IRepository.cs:40` | +| K7 | 🟢 | **Bug latente**: `IdentityGenerator.GuidRegex` contém 2 caracteres invisíveis de largura zero (U+200C e U+200B) dentro da classe `[0-9…a-fA-F]{12}` — artefato de copy-paste (confirmado por dump de bytes). Dependência morta `eQuantic.Core` (nenhum arquivo a importa). Typo `mintute` em `MigrationAttribute`. | `IdentityGenerator.cs:7`; `core-data.csproj` | + +### 7. Processo, CI e testes + +| # | Sev. | Problema | Local | +|---|------|----------|-------| +| Q1 | 🔴 | **CI publica em qualquer push, sem testes**: `on: [push]` → `dotnet nuget push` a cada push em qualquer branch, com a chave NuGet. **Nenhum `dotnet test`** roda antes de publicar. | `.github/workflows/dotnetcore.yml:3,66-67` | +| Q2 | 🔴 | **Testes são placebo**: `UnitTest1.cs` é um `Assert.Pass()`. Não há cobertura de nenhum bug acima. | `tests/…Tests/UnitTest1.cs` | +| Q3 | 🟡 | 21 `dotnet build` sequenciais em vez da solution; ações desatualizadas (`checkout@v3`, `setup-dotnet@v3`); `windows-latest` desnecessário; sem `dotnet test`, cache, `global.json`, pack determinístico (`ContinuousIntegrationBuild`), símbolos (snupkg) ou provenance. | `dotnetcore.yml` | +| Q4 | 🟡 | Sem `Directory.Build.props`/`Directory.Packages.props` centrais: metadados e versões de pacote repetidos em 21 csproj (fonte real de PK2/PK3). Sem NRT/`GenerateDocumentationFile` consistentes (pacotes vão ao NuGet sem IntelliSense). README diz "Version 4.4.0" e não explica a matriz de versões; `Repository.md` usa `IContainer`/service-locator pré-DI. | raiz do repo | + +--- + +## Parte II — Plano de execução em fases + +### Fase 0 — Blindar o pipeline (dias, sem tocar código de produção) + +Pré-requisito de tudo: parar de publicar por acidente e ter uma rede de segurança. + +1. **Separar CI de Release.** `ci.yml` em `push`/`pull_request`: `restore` → `build -warnaserror` → **`dotnet test`** → `dotnet pack` como artefato (sem push). `release.yml` só em `push: tags: ['v*']` (ou `release: published`), com a chave `nuget_key` num **GitHub Environment protegido**. +2. **Trocar os 21 builds** por `dotnet build eQuantic.Core.Data.EntityFramework.sln -c Release` (ou `dotnet pack`); mudar para `ubuntu-latest`; atualizar ações para v4; adicionar cache NuGet e `global.json`. +3. **Adotar MinVer** (versão derivada de tag git) e **deletar `build/`** (MSBump morto). Fixar `AssemblyVersion` por major. + +### Fase 1 — Correções que não quebram contrato (linha atual, patch/minor) + +Podem sair já, sem tocar o `eQuantic.Core.Data`. Cobrir cada uma com teste (Fase 0 garante que rodam). + +- **Segurança S1:** parametrizar `SqlExecutor` (usar a infra `SetCommand` já existente). +- **Crash C3:** registrar `ISqlUnitOfWork` só se a impl o implementar; remover o registro duplicado. +- **Disposal C1/C2/P7:** unificar o flag `_disposed`, não dispor o UoW injetado (ownership de quem cria), respeitar o escopo do DI. +- **Correção de queries A1, A2, A4, A5, P4:** repassar `configuration` em `All`/`Any`; validar `id is null` em vez de `default`; parametrizar a chave; fallback de `OrderBy` pela PK; `.ToArray()` no `FromSqlRaw`. +- **MongoDb P1/P2/P6:** corrigir o `GetDatabase`, rejeitar (ou traduzir para `$inc`) updates que referenciam a entidade, lançar em vez de retornar `0` silencioso. +- **Dialeto P3:** `EXEC`→`CALL` em PG/MySql. +- **Higiene M-core:** `ConfigureAwait(false)`, cache da expressão de chave em `ConcurrentDictionary`, `AddRepository` respeitando lifetime. +- **PK2/PK3/PK5:** corrigir a referência do `MySql.Net10` para o core net10; alinhar MongoDb; documentar/pinar o risco Pomelo↔EF10. +- **K7 (contrato, não-breaking):** remover os caracteres zero-width da regex, a dependência morta `eQuantic.Core`, adicionar `[AttributeUsage]`/`GenerateDocumentationFile`. + +### Fase 2 — De-duplicação estrutural (minor, refactor interno) + +Criar no pacote base `eQuantic.Core.Data.EntityFramework`: +- `SqlExecutorBase` com `GetQueryFunction`/`GetQueryProcedure` `protected abstract` (dialeto) — colapsa ~500×2 linhas. +- `UnitOfWorkBase`/`UnitOfWorkBase`, `ExpressionConverter` e o `GetQueryable`/`Load*` de `Set` no base. +- Mover `GetEntityByIdSpecification` (P8) para o base. + +Cada provider passa a ser só o override de dialeto + o `csproj`. Remove ~2.400 linhas e mata a classe de bug do P3 na raiz. **Não muda API pública** — só reorganiza a implementação. + +### Fase 3 — Redesenho dos contratos v5.0.0 (breaking deliberado — ver Parte III) + +Consolidar a superfície via *options objects*, introduzir `PagedResult`, `CancellationToken` uniforme, +NRT anotado, `where TEntity : IEntity`, remover `TUnitOfWork` da hierarquia e o conteúdo +EF-specific dos contratos. Reimplementar no pacote EF (que fica ~10× menor). + +### Fase 4 — Migração no nuget.org (ver Parte IV) + +Consolidar as linhas de versão, deprecar as antigas sem quebrar quem já depende delas, e publicar a matriz +de compatibilidade. + +--- + +## Parte III — Mudanças que exigem quebra de contrato no `eQuantic.Core.Data` + +Regra geral do pacote de contratos: **adicionar** membro a uma interface já quebra todo implementador +externo (mocks, fakes, decorators, além da própria impl EF), e **mudar assinatura/remover** quebra também +os callers. Quase toda melhoria de fundo é, portanto, uma v5.0.0. O que exige breaking: + +1. **`CancellationToken` nos membros que não têm** (30 `SumAsync`, `AddAsync`/`MergeAsync`/`ModifyAsync`/`RemoveAsync`, `LoadCollectionAsync`). Rota não-breaking parcial: *default interface methods* (DIM) delegando para a sobrecarga existente — viável porque todos os TFMs são ≥ net6.0, mas cristaliza a explosão de overloads. +2. **Consolidar overloads em options objects** (`QueryOptions` absorvendo filter/specification/config; `PageRequest`): remover os 18 `GetPagedAsync`, os 60 `Sum*` etc. Reduz `IAsyncReadRepository` de 100 para ~12 membros. É o coração da v5. +3. **`PagedResult` em vez de `IEnumerable`** na paginação: mudança de tipo de retorno — breaking duro (nem DIM salva; exigiria método novo com outro nome, ex. `QueryPagedAsync`). +4. **Remover `TUnitOfWork` da hierarquia** (colapsar `IRepository` em `IRepository` + `IUnitOfWork UnitOfWork { get; }`): remove ~10 interfaces públicas; o pacote EF referencia essas aridades em `GetRepository`. +5. **Constraint `where TEntity : IEntity`** e/ou remover `new()`: muda constraints genéricas — source+binary breaking. +6. **Separar sync/async de `IUnitOfWork`** e **remover `ExecuteTransactionAsync` de `ISqlExecutor`** (método async na interface "sync"). +7. **Mover `GetPendingMigrations`/`UpdateDatabase`/`MigrationAttribute`/`IdentityGenerator` para o pacote EF**: remove tipos/membros públicos do contrato — breaking real (a direção contrato→EF impede `[TypeForwardedTo]`). +8. **NRT anotado** (`TEntity?` em `Get`/`GetFirst`/`GetSingle`): tecnicamente só gera warnings novos — o breaking mais barato; exige que os dois pacotes sejam anotados em conjunto para ficarem coerentes. + +**Recomendação:** tratar a próxima versão do contrato como **v5.0.0 deliberadamente breaking** e reimplementar +o pacote EF sobre ela, em vez de empilhar DIMs sobre uma superfície de 100 membros. O custo de manutenção +do pacote EF — que hoje implementa ~156 membros por provider × 4 providers — cai uma ordem de magnitude. + +--- + +## Parte IV — Estratégia de versionamento no nuget.org + +O problema PK1 tem duas saídas coerentes. A recomendada é a (A). + +**(A) Consolidar numa única linha multi-target por `PackageId` (recomendado).** +Um `.csproj` multi-target por pacote (`net8.0;net9.0;net10.0` — net6/net7 estão EOL), uma única linha de +versão, retomada **acima** da mais alta já publicada para a linha do tempo voltar a ser crescente e +monotônica (ex.: **11.0.0**, ou 5.0.0 se aceitar que a "latest" numérica caia — o que confundiria quem já +está em 10.x). O multi-target já entrega o binário certo por TFM dentro de um único `.nupkg` — é +exatamente o que o esquema de linhas paralelas tenta emular à mão. Isso corrige PK1, PK2, PK4, PK5 e Q4 de +uma vez. + +**(B) Manter famílias por .NET, mas com `PackageId` distintos.** +Ex.: `eQuantic.Core.Data.EntityFramework.Net8`. É a única forma de o NuGet tratar as famílias como linhas +independentes (a "latest" de cada id fica correta para seu TFM). Custo: fragmenta o ecossistema de +consumidores e a descoberta no nuget.org, e multiplica os pacotes a manter. Só vale se houver uma razão +forte para congelar cada TFM numa API própria. + +**Migração sem quebrar quem já depende das versões antigas** (vale para A e B): +- **Nunca** despublicar (`unlist` mantém o restore de quem tem a versão fixada; `delete` quebra). Usar + **deprecação** no nuget.org (`Legacy`/`Other`) nas versões antigas, apontando para a nova. +- Publicar a **matriz de compatibilidade** (TFM × pacote × versão) no README — hoje o README diz + "Version 4.4.0" e não explica nada disso. +- Alinhar core + 4 providers para lançarem **sempre juntos, na mesma versão** (resolve os diamantes de + `AssemblyVersion`). +- Só então retomar a numeração consolidada e apontar o CI de release para tags. + +--- + +## Apêndice — Ordem sugerida (o que fazer primeiro) + +1. **Fase 0** (pipeline) — desbloqueia tudo com segurança. +2. **S1, C1, C2, C3, P1, P2** — os 6 achados 🔴 de segurança/runtime, com testes. +3. **Fase 1 restante** (achados 🟠) na mesma linha atual. +4. **Fase 2** (de-dup) — barato e alto retorno, sem breaking. +5. **Fases 3–4** — planejar a v5.0.0 do contrato e a consolidação de versões como um marco à parte, + comunicado com antecedência aos consumidores. From 77121e59a6c3dec9562e329d438c7202c2f0bd9e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:24:43 +0000 Subject: [PATCH 02/32] =?UTF-8?q?=F0=9F=94=92=20fix(sql):=20parameterize?= =?UTF-8?q?=20SqlExecutor=20to=20close=20SQL=20injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetQueryParameters interpolated parameter values straight into the SQL text (string.Format(" '{0}'", value) with no escaping), and the result was passed to FromSqlRaw with no DbParameters — a SQL injection in ExecuteFunction/ExecuteProcedure across the SqlServer, PostgreSql and MySql providers. - Emit positional placeholders ({0},{1}) for the FromSqlRaw function path and named placeholders (@Param0/@Name) for the DbCommand procedure path, matching the parameters SetCommand already creates. Values now travel as DbParameters and are never interpolated. - ExecuteProcedure (sync) now forwards config so its parameters are bound. - ExecuteQuery: materialize the values with ToArray() so N parameters are passed as N arguments instead of a single IEnumerable (P4). - PostgreSql/MySql procedures now use CALL instead of the T-SQL EXEC that was copied verbatim from the SqlServer provider (P3). - Add unit tests proving a malicious value never reaches the generated SQL. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Repository/SqlExecutor.cs | 89 +++++++++++++------ .../Repository/SqlExecutor.cs | 89 +++++++++++++------ .../Repository/SqlExecutor.cs | 88 ++++++++++++------ .../SqlExecutorParameterizationTests.cs | 75 ++++++++++++++++ 4 files changed, 254 insertions(+), 87 deletions(-) create mode 100644 tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs index 5020a7b..dd04cba 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs @@ -131,7 +131,7 @@ public TResult ExecuteFunction(string name, Action() - .FromSqlRaw(sql) + .FromSqlRaw(sql, GetParameterValues(configuration)) .FirstOrDefault(); } @@ -189,7 +189,7 @@ public Task ExecuteFunctionAsync(string name, { var configuration = GetConfig(config); var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set().FromSqlRaw(sql) + return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)) .FirstOrDefaultAsync(cancellationToken); } @@ -202,7 +202,7 @@ public Task ExecuteFunctionAsync(string name, public int ExecuteProcedure(string name, Action config = null) { var configuration = GetConfig(config); - return ExecuteCommand(GetQueryProcedure(name, configuration) + ";"); + return ExecuteCommand(GetQueryProcedure(name, configuration) + ";", config); } /// @@ -216,7 +216,7 @@ public IEnumerable ExecuteQuery(string sqlQuery, Action().FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value)); + return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)); } /// @@ -346,33 +346,64 @@ public Task UseTransactionAsync(DbTransaction transaction, CancellationToken can } /// - /// Gets the query function using the specified name + /// Gets the query function using the specified name. Parameter values are emitted as + /// positional placeholders ({0}, {1}, …) so the values travel as + /// s through FromSqlRaw and are never + /// interpolated into the SQL text. /// /// The name /// The configuration. /// The string - private static string GetQueryFunction(string name, SqlConfiguration config) + internal static string GetQueryFunction(string name, SqlConfiguration config) { - return $"SELECT {name}({GetQueryParameters(config.Parameters.ToArray())} )"; + return $"SELECT {name}({GetPositionalPlaceholders(config.Parameters.Count)} )"; } - + /// - /// Gets the query procedure using the specified name + /// Gets the query procedure using the specified name. MySQL invokes stored procedures with + /// CALL; parameter values are emitted as named placeholders matching the + /// s created by , so the values are never + /// interpolated into the SQL text. /// /// The name /// The configuration. /// The string - private static string GetQueryProcedure(string name, SqlConfiguration config) + internal static string GetQueryProcedure(string name, SqlConfiguration config) { - return $"EXEC {name}{GetQueryParameters(config.Parameters.ToArray())}"; + return $"CALL {name}({GetNamedPlaceholders(config.Parameters.ToArray())} )"; } - + /// - /// Gets the query parameters using the specified parameters + /// Builds a comma-separated list of positional placeholders ({0}, {1}, …) for the + /// given parameter count. Used by FromSqlRaw, which substitutes each placeholder with a + /// parameter reference. + /// + /// The number of parameters. + /// The placeholder string. + internal static string GetPositionalPlaceholders(int count) + { + var cmdBuilder = new StringBuilder(); + for (var i = 0; i < count; i++) + { + if (i > 0) + { + cmdBuilder.Append(','); + } + + cmdBuilder.Append(" {").Append(i).Append('}'); + } + + return cmdBuilder.ToString(); + } + + /// + /// Builds a comma-separated list of named placeholders (@Param0, @Name, …) using + /// the same naming convention as , so the placeholders bind to the + /// parameters added to the command. /// /// The parameters - /// The string - private static string GetQueryParameters(params ParamValue[] parameters) + /// The placeholder string. + internal static string GetNamedPlaceholders(params ParamValue[] parameters) { var cmdBuilder = new StringBuilder(); if (parameters is not { Length: > 0 }) @@ -387,25 +418,25 @@ private static string GetQueryParameters(params ParamValue[] parameters) cmdBuilder.Append(','); } - if (parameters[i].Value == null) - { - cmdBuilder.Append(" NULL"); - } - else - { - var fmt = " {0}"; - if (parameters[i].Value is Guid or string or DateTime) - { - fmt = " '{0}'"; - } - - cmdBuilder.Append(string.Format(fmt, parameters[i].Value)); - } + var parameterName = string.IsNullOrEmpty(parameters[i].Name) ? $"Param{i}" : parameters[i].Name; + cmdBuilder.Append(" @").Append(parameterName); } return cmdBuilder.ToString(); } + /// + /// Gets the ordered parameter values used to feed FromSqlRaw's positional placeholders. + /// + /// The configuration. + /// The parameter values, in the same order as the emitted placeholders. + internal static object[] GetParameterValues(SqlConfiguration config) + { + return config.Parameters == null + ? Array.Empty() + : config.Parameters.Select(p => p.Value).ToArray(); + } + private static string ParseSql(string sql, SqlConfiguration config) { return !string.IsNullOrEmpty(config.Tag) ? GetQueryWithTag(sql, config.Tag) : sql; diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs index 25ec1a0..725d750 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs @@ -131,7 +131,7 @@ public TResult ExecuteFunction(string name, Action() - .FromSqlRaw(sql) + .FromSqlRaw(sql, GetParameterValues(configuration)) .FirstOrDefault(); } @@ -189,7 +189,7 @@ public Task ExecuteFunctionAsync(string name, { var configuration = GetConfig(config); var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set().FromSqlRaw(sql) + return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)) .FirstOrDefaultAsync(cancellationToken); } @@ -202,7 +202,7 @@ public Task ExecuteFunctionAsync(string name, public int ExecuteProcedure(string name, Action config = null) { var configuration = GetConfig(config); - return ExecuteCommand(GetQueryProcedure(name, configuration) + ";"); + return ExecuteCommand(GetQueryProcedure(name, configuration) + ";", config); } /// @@ -216,7 +216,7 @@ public IEnumerable ExecuteQuery(string sqlQuery, Action().FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value)); + return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)); } /// @@ -346,33 +346,64 @@ public Task UseTransactionAsync(DbTransaction transaction, CancellationToken can } /// - /// Gets the query function using the specified name + /// Gets the query function using the specified name. Parameter values are emitted as + /// positional placeholders ({0}, {1}, …) so the values travel as + /// s through FromSqlRaw and are never + /// interpolated into the SQL text. /// /// The name /// The configuration. /// The string - private static string GetQueryFunction(string name, SqlConfiguration config) + internal static string GetQueryFunction(string name, SqlConfiguration config) { - return $"SELECT {name}({GetQueryParameters(config.Parameters.ToArray())} )"; + return $"SELECT {name}({GetPositionalPlaceholders(config.Parameters.Count)} )"; } - + /// - /// Gets the query procedure using the specified name + /// Gets the query procedure using the specified name. PostgreSQL invokes stored procedures with + /// CALL; parameter values are emitted as named placeholders matching the + /// s created by , so the values are never + /// interpolated into the SQL text. /// /// The name /// The configuration. /// The string - private static string GetQueryProcedure(string name, SqlConfiguration config) + internal static string GetQueryProcedure(string name, SqlConfiguration config) { - return $"EXEC {name}{GetQueryParameters(config.Parameters.ToArray())}"; + return $"CALL {name}({GetNamedPlaceholders(config.Parameters.ToArray())} )"; } - + /// - /// Gets the query parameters using the specified parameters + /// Builds a comma-separated list of positional placeholders ({0}, {1}, …) for the + /// given parameter count. Used by FromSqlRaw, which substitutes each placeholder with a + /// parameter reference. + /// + /// The number of parameters. + /// The placeholder string. + internal static string GetPositionalPlaceholders(int count) + { + var cmdBuilder = new StringBuilder(); + for (var i = 0; i < count; i++) + { + if (i > 0) + { + cmdBuilder.Append(','); + } + + cmdBuilder.Append(" {").Append(i).Append('}'); + } + + return cmdBuilder.ToString(); + } + + /// + /// Builds a comma-separated list of named placeholders (@Param0, @Name, …) using + /// the same naming convention as , so the placeholders bind to the + /// parameters added to the command. /// /// The parameters - /// The string - private static string GetQueryParameters(params ParamValue[] parameters) + /// The placeholder string. + internal static string GetNamedPlaceholders(params ParamValue[] parameters) { var cmdBuilder = new StringBuilder(); if (parameters is not { Length: > 0 }) @@ -387,25 +418,25 @@ private static string GetQueryParameters(params ParamValue[] parameters) cmdBuilder.Append(','); } - if (parameters[i].Value == null) - { - cmdBuilder.Append(" NULL"); - } - else - { - var fmt = " {0}"; - if (parameters[i].Value is Guid or string or DateTime) - { - fmt = " '{0}'"; - } - - cmdBuilder.Append(string.Format(fmt, parameters[i].Value)); - } + var parameterName = string.IsNullOrEmpty(parameters[i].Name) ? $"Param{i}" : parameters[i].Name; + cmdBuilder.Append(" @").Append(parameterName); } return cmdBuilder.ToString(); } + /// + /// Gets the ordered parameter values used to feed FromSqlRaw's positional placeholders. + /// + /// The configuration. + /// The parameter values, in the same order as the emitted placeholders. + internal static object[] GetParameterValues(SqlConfiguration config) + { + return config.Parameters == null + ? Array.Empty() + : config.Parameters.Select(p => p.Value).ToArray(); + } + private static string ParseSql(string sql, SqlConfiguration config) { return !string.IsNullOrEmpty(config.Tag) ? GetQueryWithTag(sql, config.Tag) : sql; diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs index d6b9473..bc77e31 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs @@ -131,7 +131,7 @@ public TResult ExecuteFunction(string name, Action() - .FromSqlRaw(sql) + .FromSqlRaw(sql, GetParameterValues(configuration)) .FirstOrDefault(); } @@ -189,7 +189,7 @@ public Task ExecuteFunctionAsync(string name, { var configuration = GetConfig(config); var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set().FromSqlRaw(sql) + return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)) .FirstOrDefaultAsync(cancellationToken); } @@ -202,7 +202,7 @@ public Task ExecuteFunctionAsync(string name, public int ExecuteProcedure(string name, Action config = null) { var configuration = GetConfig(config); - return ExecuteCommand(GetQueryProcedure(name, configuration) + ";"); + return ExecuteCommand(GetQueryProcedure(name, configuration) + ";", config); } /// @@ -216,7 +216,7 @@ public IEnumerable ExecuteQuery(string sqlQuery, Action().FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value)); + return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)); } /// @@ -346,33 +346,63 @@ public Task UseTransactionAsync(DbTransaction transaction, CancellationToken can } /// - /// Gets the query function using the specified name + /// Gets the query function using the specified name. Parameter values are emitted as + /// positional placeholders ({0}, {1}, …) so the values travel as + /// s through FromSqlRaw and are never + /// interpolated into the SQL text. /// /// The name /// The configuration. /// The string - private static string GetQueryFunction(string name, SqlConfiguration config) + internal static string GetQueryFunction(string name, SqlConfiguration config) { - return $"SELECT {name}({GetQueryParameters(config.Parameters.ToArray())} )"; + return $"SELECT {name}({GetPositionalPlaceholders(config.Parameters.Count)} )"; } - + /// - /// Gets the query procedure using the specified name + /// Gets the query procedure using the specified name. Parameter values are emitted as named + /// placeholders matching the s created by , + /// so the values are never interpolated into the SQL text. /// /// The name /// The configuration. /// The string - private static string GetQueryProcedure(string name, SqlConfiguration config) + internal static string GetQueryProcedure(string name, SqlConfiguration config) { - return $"EXEC {name}{GetQueryParameters(config.Parameters.ToArray())}"; + return $"EXEC {name}{GetNamedPlaceholders(config.Parameters.ToArray())}"; } - + /// - /// Gets the query parameters using the specified parameters + /// Builds a comma-separated list of positional placeholders ({0}, {1}, …) for the + /// given parameter count. Used by FromSqlRaw, which substitutes each placeholder with a + /// parameter reference. + /// + /// The number of parameters. + /// The placeholder string. + internal static string GetPositionalPlaceholders(int count) + { + var cmdBuilder = new StringBuilder(); + for (var i = 0; i < count; i++) + { + if (i > 0) + { + cmdBuilder.Append(','); + } + + cmdBuilder.Append(" {").Append(i).Append('}'); + } + + return cmdBuilder.ToString(); + } + + /// + /// Builds a comma-separated list of named placeholders (@Param0, @Name, …) using + /// the same naming convention as , so the placeholders bind to the + /// parameters added to the command. /// /// The parameters - /// The string - private static string GetQueryParameters(params ParamValue[] parameters) + /// The placeholder string. + internal static string GetNamedPlaceholders(params ParamValue[] parameters) { var cmdBuilder = new StringBuilder(); if (parameters is not { Length: > 0 }) @@ -387,25 +417,25 @@ private static string GetQueryParameters(params ParamValue[] parameters) cmdBuilder.Append(','); } - if (parameters[i].Value == null) - { - cmdBuilder.Append(" NULL"); - } - else - { - var fmt = " {0}"; - if (parameters[i].Value is Guid or string or DateTime) - { - fmt = " '{0}'"; - } - - cmdBuilder.Append(string.Format(fmt, parameters[i].Value)); - } + var parameterName = string.IsNullOrEmpty(parameters[i].Name) ? $"Param{i}" : parameters[i].Name; + cmdBuilder.Append(" @").Append(parameterName); } return cmdBuilder.ToString(); } + /// + /// Gets the ordered parameter values used to feed FromSqlRaw's positional placeholders. + /// + /// The configuration. + /// The parameter values, in the same order as the emitted placeholders. + internal static object[] GetParameterValues(SqlConfiguration config) + { + return config.Parameters == null + ? Array.Empty() + : config.Parameters.Select(p => p.Value).ToArray(); + } + private static string ParseSql(string sql, SqlConfiguration config) { return !string.IsNullOrEmpty(config.Tag) ? GetQueryWithTag(sql, config.Tag) : sql; diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs new file mode 100644 index 0000000..bf1520f --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs @@ -0,0 +1,75 @@ +using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; +using eQuantic.Core.Data.Repository.Config; + +namespace eQuantic.Core.Data.EntityFramework.SqlServer.Tests; + +/// +/// Guards the fix for the SQL-injection defect: parameter values must be emitted as placeholders, +/// never interpolated into the SQL text. +/// +public class SqlExecutorParameterizationTests +{ + private const string Malicious = "'; DROP TABLE Users; --"; + + [Test] + public void GetQueryFunction_EmitsPositionalPlaceholders_NotValues() + { + var config = new DefaultSqlConfiguration().WithParameters(Malicious, 42); + + var sql = SqlExecutor.GetQueryFunction("dbo.GetUser", config); + + Assert.That(sql, Does.Contain("{0}")); + Assert.That(sql, Does.Contain("{1}")); + Assert.That(sql, Does.Not.Contain("DROP TABLE")); + Assert.That(sql, Does.Not.Contain(Malicious)); + } + + [Test] + public void GetQueryProcedure_EmitsNamedPlaceholders_NotValues() + { + var config = new DefaultSqlConfiguration().WithParameters(Malicious, 42); + + var sql = SqlExecutor.GetQueryProcedure("dbo.DoWork", config); + + Assert.That(sql, Does.StartWith("EXEC dbo.DoWork")); + Assert.That(sql, Does.Contain("@Param0")); + Assert.That(sql, Does.Contain("@Param1")); + Assert.That(sql, Does.Not.Contain("DROP TABLE")); + Assert.That(sql, Does.Not.Contain(Malicious)); + } + + [Test] + public void GetQueryProcedure_UsesProvidedParameterNames() + { + var config = new DefaultSqlConfiguration() + .WithParameters(ParamValueFor("userId", 7), ParamValueFor("state", "active")); + + var sql = SqlExecutor.GetQueryProcedure("dbo.DoWork", config); + + Assert.That(sql, Does.Contain("@userId")); + Assert.That(sql, Does.Contain("@state")); + } + + [Test] + public void GetParameterValues_PreservesValues_ForPositionalBinding() + { + var config = new DefaultSqlConfiguration().WithParameters(Malicious, 42); + + var values = SqlExecutor.GetParameterValues(config); + + Assert.That(values, Has.Length.EqualTo(2)); + Assert.That(values, Does.Contain(Malicious)); + Assert.That(values, Does.Contain(42)); + } + + [Test] + public void GetPositionalPlaceholders_EmptyForNoParameters() + { + var sql = SqlExecutor.GetQueryFunction("dbo.NoArgs", new DefaultSqlConfiguration()); + + Assert.That(sql, Does.Not.Contain("{0}")); + } + + private static eQuantic.Core.Data.Repository.Sql.ParamValue ParamValueFor(string name, object value) + => eQuantic.Core.Data.Repository.Sql.ParamValue.Create(name, value); +} From efe597f85c5b1329fe12dfbfbf28fd08b354be54 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:32:39 +0000 Subject: [PATCH 03/32] =?UTF-8?q?=F0=9F=90=9E=20fix(core):=20correct=20DI?= =?UTF-8?q?=20registration=20and=20double-dispose=20of=20UnitOfWork?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C3 — ServiceCollectionExtensions registered ISqlUnitOfWork against every unit of work unconditionally, so resolving it threw InvalidCastException for non-relational unit of works (MongoDb, which implements IQueryableUnitOfWork but not ISqlUnitOfWork). Gate the registration on the implementation type, and drop the duplicate IQueryableUnitOfWork TryAdd. C1 — AsyncQueryableRepository shadowed the base _disposed field, so both the base and derived Dispose(bool) blocks ran and disposed the injected UnitOfWork twice. Promote the flag to a shared protected field, dispose only the async sub-repositories in the override, and delegate the unit of work disposal to the base so it happens exactly once. Replace the placebo Assert.Pass() test with real coverage: DI-registration tests and disposal tests (dispose-once + idempotency), plus test fakes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Repository/AsyncQueryableRepository.cs | 10 ++- .../Extensions/ServiceCollectionExtensions.cs | 9 ++- .../Repository/QueryableRepository.cs | 12 +++- .../Fakes/FakeEntity.cs | 12 ++++ .../Fakes/FakeQueryableUnitOfWork.cs | 61 +++++++++++++++++++ .../RepositoryDisposalTests.cs | 35 +++++++++++ .../ServiceCollectionExtensionsTests.cs | 51 ++++++++++++++++ .../UnitTest1.cs | 15 ----- ...tic.Core.Data.EntityFramework.Tests.csproj | 2 + 9 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeEntity.cs create mode 100644 tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs create mode 100644 tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs create mode 100644 tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs delete mode 100644 tests/eQuantic.Core.Data.EntityFramework.Tests/UnitTest1.cs diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs index be7a1b7..b33fe2a 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs @@ -24,7 +24,6 @@ public class AsyncQueryableRepository : { private readonly IAsyncQueryableReadRepository _asyncReadRepository; private readonly IAsyncWriteRepository _asyncWriteRepository; - private bool _disposed; public AsyncQueryableRepository(TUnitOfWork unitOfWork) : base(unitOfWork) { @@ -756,9 +755,7 @@ public Task UpdateManyAsync(ISpecification specification, protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - if (_disposed) + if (Disposed) { return; } @@ -767,9 +764,10 @@ protected override void Dispose(bool disposing) { this._asyncReadRepository?.Dispose(); this._asyncWriteRepository?.Dispose(); - UnitOfWork?.Dispose(); } - _disposed = true; + // Base disposes the sync sub-repositories and the unit of work, and flips the shared + // Disposed flag. The unit of work is therefore disposed exactly once. + base.Dispose(disposing); } } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs index 27daf7b..9d0e1f8 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs @@ -71,8 +71,13 @@ private static void AddUnitOfWork(IServic services.TryAdd(new ServiceDescriptor(typeof(TUnitOfWorkInterface), typeof(TUnitOfWorkImpl), lifetime)); services.TryAdd(new ServiceDescriptor(typeof(IQueryableUnitOfWork), sp => sp.GetRequiredService(), lifetime)); - services.TryAdd(new ServiceDescriptor(typeof(ISqlUnitOfWork), sp => sp.GetRequiredService(), lifetime)); - services.TryAdd(new ServiceDescriptor(typeof(IQueryableUnitOfWork), sp => sp.GetRequiredService(), lifetime)); + // Only expose ISqlUnitOfWork when the implementation actually provides it. Non-relational + // unit of works (e.g. MongoDb) implement IQueryableUnitOfWork but not ISqlUnitOfWork; + // registering it unconditionally made resolving ISqlUnitOfWork throw InvalidCastException. + if (typeof(ISqlUnitOfWork).IsAssignableFrom(typeof(TUnitOfWorkImpl))) + { + services.TryAdd(new ServiceDescriptor(typeof(ISqlUnitOfWork), sp => sp.GetRequiredService(), lifetime)); + } } private static void AddGenericRepositories(IServiceCollection services, ServiceLifetime lifetime) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs index d25c758..1c1f979 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs @@ -21,7 +21,13 @@ public class QueryableRepository : { private readonly IQueryableReadRepository _readRepository; private readonly IWriteRepository _writeRepository; - private bool _disposed; + + /// + /// Shared disposal flag. Kept protected so derived repositories observe the same state + /// instead of shadowing it — shadowing let both levels run their disposal block and dispose the + /// unit of work twice. + /// + protected bool Disposed; /// /// Create a new instance of repository @@ -362,7 +368,7 @@ public long UpdateMany(ISpecification specification, Expression +/// Minimal entity used to close the generic type parameters of the repositories under test. +/// +internal sealed class FakeEntity : IEntity +{ + public int Id { get; set; } + public string? Name { get; set; } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs new file mode 100644 index 0000000..fb679d6 --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Options; + +namespace eQuantic.Core.Data.EntityFramework.Tests.Fakes; + +/// +/// A non-relational unit of work: it implements but NOT +/// — mirroring the MongoDb provider. +/// Members throw because the DI-registration tests only inspect the service collection; the fake is +/// never instantiated or resolved. +/// +internal sealed class FakeQueryableUnitOfWork : IQueryableUnitOfWork +{ + public int DisposeCount { get; private set; } + + public void Dispose() => DisposeCount++; + + public int Commit() => throw new NotSupportedException(); + public int Commit(Action options) => throw new NotSupportedException(); + public int CommitAndRefreshChanges() => throw new NotSupportedException(); + public int CommitAndRefreshChanges(Action options) => throw new NotSupportedException(); + + public Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public Task CommitAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException(); + + public Task CommitAsync(Action options, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public void RollbackChanges() => throw new NotSupportedException(); + + public IRepository GetRepository() + where TEntity : class, IEntity, new() + where TUnitOfWork : IUnitOfWork + => throw new NotSupportedException(); + + public IAsyncRepository GetAsyncRepository() + where TEntity : class, IEntity, new() + where TUnitOfWork : IUnitOfWork + => throw new NotSupportedException(); + + public eQuantic.Core.Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() + => throw new NotSupportedException(); + + public IQueryableRepository GetQueryableRepository() + where TEntity : class, IEntity, new() + where TUnitOfWork : IQueryableUnitOfWork + => throw new NotSupportedException(); + + public IAsyncQueryableRepository GetAsyncQueryableRepository() + where TEntity : class, IEntity, new() + where TUnitOfWork : IQueryableUnitOfWork + => throw new NotSupportedException(); +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs new file mode 100644 index 0000000..d12a851 --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs @@ -0,0 +1,35 @@ +using eQuantic.Core.Data.EntityFramework.Repository; +using eQuantic.Core.Data.EntityFramework.Tests.Fakes; + +namespace eQuantic.Core.Data.EntityFramework.Tests; + +/// +/// Guards the fix for the double-dispose defect: disposing an +/// must dispose its unit of work exactly once, not twice (the shadowed disposal flag made both the +/// base and derived disposal blocks run). +/// +public class RepositoryDisposalTests +{ + [Test] + public void Dispose_AsyncQueryableRepository_DisposesUnitOfWork_Once() + { + var unitOfWork = new FakeQueryableUnitOfWork(); + var repository = new AsyncQueryableRepository(unitOfWork); + + repository.Dispose(); + + Assert.That(unitOfWork.DisposeCount, Is.EqualTo(1)); + } + + [Test] + public void Dispose_AsyncQueryableRepository_IsIdempotent() + { + var unitOfWork = new FakeQueryableUnitOfWork(); + var repository = new AsyncQueryableRepository(unitOfWork); + + repository.Dispose(); + repository.Dispose(); + + Assert.That(unitOfWork.DisposeCount, Is.EqualTo(1)); + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..328dc6b --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,51 @@ +using System.Linq; +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Tests.Fakes; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Sql; +using Microsoft.Extensions.DependencyInjection; + +namespace eQuantic.Core.Data.EntityFramework.Tests; + +/// +/// Guards the fix for the DI-registration defect: a non-relational unit of work must not have +/// registered against it, and +/// must be registered exactly once. +/// +public class ServiceCollectionExtensionsTests +{ + [Test] + public void AddQueryableRepositories_NonSqlUnitOfWork_DoesNotRegisterSqlUnitOfWork() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + Assert.That(services.Any(d => d.ServiceType == typeof(ISqlUnitOfWork)), Is.False, + "ISqlUnitOfWork must not be registered for a unit of work that does not implement it."); + } + + [Test] + public void AddQueryableRepositories_RegistersQueryableUnitOfWork_ExactlyOnce() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + Assert.That(services.Count(d => d.ServiceType == typeof(IQueryableUnitOfWork)), Is.EqualTo(1), + "IQueryableUnitOfWork must be registered exactly once (the duplicate TryAdd was removed)."); + } + + [Test] + public void AddQueryableRepositories_RegistersConcreteUnitOfWork_AsQueryableUnitOfWork() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + Assert.That( + services.Any(d => d.ServiceType == typeof(IQueryableUnitOfWork) + && d.ImplementationType == typeof(FakeQueryableUnitOfWork)), + Is.True); + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/UnitTest1.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/UnitTest1.cs deleted file mode 100644 index 5fcc4bb..0000000 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/UnitTest1.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace eQuantic.Core.Data.EntityFramework.Tests; - -public class Tests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } -} \ No newline at end of file diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj index a8a9423..e777726 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj @@ -10,6 +10,8 @@ + + From 0e8d540c6c35997e3362ae206d3d9cd2f08865d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:38:17 +0000 Subject: [PATCH 04/32] =?UTF-8?q?=F0=9F=90=9E=20fix(read):=20honour=20conf?= =?UTF-8?q?iguration=20in=20All/Any=20and=20accept=20default=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A1 — QueryableReadRepository.All(spec, config) and Any(spec, config) dropped the caller's configuration (includes, no-tracking, sorting, query filters) by delegating to the filter overload without it. Forward the configuration, matching what the async siblings already do. A2 — Get(id)/GetAsync(id) rejected any default-valued key with ArgumentNullException, so Get(0), Guid.Empty, etc. threw on an argument that is not null. Guard on `id is null` so only reference-type nulls are rejected and default value-type keys reach the lookup. Add EF Core InMemory integration tests covering both fixes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Read/AsyncQueryableReadRepository.cs | 2 +- .../Read/QueryableReadRepository.cs | 6 +- .../ReadRepositoryQueryTests.cs | 107 ++++++++++++++++++ ...ata.EntityFramework.SqlServer.Tests.csproj | 2 + 4 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs index 153ea46..5fe4e5a 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs @@ -354,7 +354,7 @@ public Task GetAsync( Action> configuration, CancellationToken cancellationToken) { - if (Equals(id, default(TKey))) + if (id is null) { throw new ArgumentNullException(nameof(id)); } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs index 243def1..a34c02c 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs @@ -205,7 +205,7 @@ public bool All(ISpecification specification, Action> filter, Action> configuration = default) @@ -230,7 +230,7 @@ public bool Any(ISpecification specification, Action> filter, Action> configuration = default) @@ -251,7 +251,7 @@ public void Dispose() public TEntity Get(TKey id, Action> configuration = default) { - if (Equals(id, default(TKey))) + if (id is null) { throw new ArgumentNullException(nameof(id)); } diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs new file mode 100644 index 0000000..1ae3c48 --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.Repository.Read; +using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; +using eQuantic.Core.Data.Repository.Config; +using eQuantic.Linq.Specification; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace eQuantic.Core.Data.EntityFramework.SqlServer.Tests; + +/// +/// Integration coverage (EF Core InMemory) for the read-repository query fixes: +/// A1 — All/Any with a specification must honour the caller's configuration. +/// A2 — Get must not reject a default-valued key (e.g. 0) as a null argument. +/// +public class ReadRepositoryQueryTests +{ + private sealed class Product : eQuantic.Core.Data.Repository.IEntity + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } + + private sealed class TestDbContext(DbContextOptions options) : DbContext(options) + { + public DbSet Products => Set(); + } + + private sealed class NameSpecification(string name) : Specification + { + public override Expression> SatisfiedBy() => p => p.Name == name; + } + + private static DefaultUnitOfWork NewUnitOfWork(out TestDbContext context) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase($"read-repo-{Guid.NewGuid():N}") + .Options; + context = new TestDbContext(options); + return new DefaultUnitOfWork(new ServiceCollection().BuildServiceProvider(), context); + } + + private static QueryableReadRepository NewRepository(out TestDbContext context) + => new(NewUnitOfWork(out context)); + + [Test] + public void All_WithSpecification_HonoursConfiguration() + { + var repository = NewRepository(out var context); + context.Products.AddRange( + new Product { Id = 1, Name = "active" }, + new Product { Id = 2, Name = "inactive" }); + context.SaveChanges(); + + // The "inactive" row does not satisfy the specification, so without the configuration being + // applied All() would evaluate over both rows and return false. The configuration narrows the + // query to the "active" row, so the fixed code returns true. + var result = repository.All( + new NameSpecification("active"), + cfg => cfg.WithAfterCustomization(q => q.Where(p => p.Name == "active"))); + + Assert.That(result, Is.True); + } + + [Test] + public void Any_WithSpecification_HonoursConfiguration() + { + var repository = NewRepository(out var context); + context.Products.Add(new Product { Id = 1, Name = "active" }); + context.SaveChanges(); + + // The configuration filters out every row, so Any() must return false once it is applied. + var result = repository.Any( + new NameSpecification("active"), + cfg => cfg.WithAfterCustomization(q => q.Where(_ => false))); + + Assert.That(result, Is.False); + } + + [Test] + public void Get_WithDefaultValuedKey_DoesNotThrow() + { + var repository = NewRepository(out var context); + context.Products.Add(new Product { Id = 1, Name = "active" }); + context.SaveChanges(); + + // Key 0 is a valid (if absent) value for a value-typed key; it must not be treated as null. + Product? found = null; + Assert.DoesNotThrow(() => found = repository.Get(0)); + Assert.That(found, Is.Null); + } + + [Test] + public void Get_WithExistingKey_ReturnsEntity() + { + var repository = NewRepository(out var context); + context.Products.Add(new Product { Id = 5, Name = "found" }); + context.SaveChanges(); + + var found = repository.Get(5); + + Assert.That(found, Is.Not.Null); + Assert.That(found!.Name, Is.EqualTo("found")); + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj index 588e730..ce2b8e0 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj @@ -10,6 +10,8 @@ + + From d874197f86314904cf8af23921bbad666732614b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:40:57 +0000 Subject: [PATCH 05/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20record=20Phase=201?= =?UTF-8?q?=20implementation=20status=20in=20improvement=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- docs/IMPROVEMENT_PLAN.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md index d448dd7..c8b3ee9 100644 --- a/docs/IMPROVEMENT_PLAN.md +++ b/docs/IMPROVEMENT_PLAN.md @@ -31,6 +31,23 @@ O plano abaixo está em **5 fases**. As Fases 0–2 **não quebram contrato** e As Fases 3–4 definem a **v5.0.0 dos contratos** (breaking deliberado) e a estratégia de versionamento no nuget.org. +## Status de implementação (Fase 1 iniciada) + +Correções já aplicadas nesta branch, com testes (17 testes passando, todos os 5 pacotes compilando em net10): + +| Achado | Correção | Commit | +|--------|----------|--------| +| **S1** injeção de SQL + **P4** + **P3** | `SqlExecutor` parametrizado (placeholders em vez de interpolar valores) nos 3 providers SQL; `EXEC`→`CALL` em PG/MySql; `.ToArray()` no `ExecuteQuery`. Testes provam que o valor malicioso não chega ao SQL. | `🔒 fix(sql)` | +| **C3** crash de DI | `ISqlUnitOfWork` só registrado quando a impl o implementa; removido registro duplicado de `IQueryableUnitOfWork`. | `🐞 fix(core)` | +| **C1** double-dispose | `_disposed` unificado (`protected`); UoW disposto exatamente uma vez. | `🐞 fix(core)` | +| **A1** config descartada | `All`/`Any` sync repassam `configuration`. | `🐞 fix(read)` | +| **A2** chave default | `Get`/`GetAsync` validam `id is null` em vez de `default(TKey)`. | `🐞 fix(read)` | + +Substituído o teste placebo (`Assert.Pass()`) por cobertura real (DI, disposal, parametrização de SQL, +queries via EF InMemory). **Pendente na Fase 1** (próximos passos): C2 (ownership do UoW injetado — +comportamental, precisa de decisão), P1/P2 (MongoDb banco errado / update de constante), M-core +(`ConfigureAwait(false)`, cache de expressão de chave, `AddRepository` respeitando lifetime), A4/A5. + --- ## Parte I — Diagnóstico From 346a20f5a2552ad8fa89f1a9c5e9847f5c868257 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:46:56 +0000 Subject: [PATCH 06/32] =?UTF-8?q?=F0=9F=90=9E=20fix(mongodb):=20target=20t?= =?UTF-8?q?he=20configured=20database=20and=20fail=20loudly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetCollection resolved the database via GetDatabase(_collectionName), using the collection name as the database name — so DeleteMany/UpdateMany ran against a wrong/non-existent database and silently reported zero affected documents. Resolve the database name from the DbContext's MongoOptionsExtension (UseMongoDB(..., databaseName)) instead, and throw a descriptive InvalidOperationException when the database name or the IMongoClient cannot be resolved, rather than returning 0 and hiding the misconfiguration. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Repository/Set.cs | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs index d51ccc0..21ffb11 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs @@ -5,9 +5,11 @@ using eQuantic.Core.Data.Repository.Config; using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using MongoDB.EntityFrameworkCore.Extensions; +using MongoDB.EntityFrameworkCore.Infrastructure; namespace eQuantic.Core.Data.EntityFramework.MongoDb.Repository; @@ -15,62 +17,56 @@ namespace eQuantic.Core.Data.EntityFramework.MongoDb.Repository; { private readonly IServiceProvider _serviceProvider; private readonly string? _collectionName; + private readonly string? _databaseName; private IMongoDatabase? _mongoDatabase; - + public Set(IServiceProvider serviceProvider, DbContext context) : base(context) { _serviceProvider = serviceProvider; var entityType = context.Model.FindEntityType(typeof(TEntity)); _collectionName = entityType?.GetCollectionName(); + + // Resolve the database name from the DbContext configuration up front (the context is only + // available here), NOT from the collection name. + _databaseName = context.GetService() + .FindExtension()?.DatabaseName; } public override long DeleteMany(Expression> filter) { - var collection = GetCollection(); - if (collection == null) return 0; - - var result = collection.DeleteMany(filter); + var result = GetCollection().DeleteMany(filter); return result.DeletedCount; } public override async Task DeleteManyAsync(Expression> filter, CancellationToken cancellationToken = default) { - var collection = GetCollection(); - if (collection == null) return 0; - var mongoFilter = Builders.Filter.Where(filter); - var result = await collection.DeleteManyAsync(mongoFilter, cancellationToken); - + var result = await GetCollection().DeleteManyAsync(mongoFilter, cancellationToken); + return result.DeletedCount; } public override long UpdateMany(Expression> filter, Expression> updateExpression) { - var collection = GetCollection(); - if (collection == null) return 0; - var mongoFilter = Builders.Filter.Where(filter); var updateDefinition = UpdateDefinitionBuilder.BuildUpdateDefinition(updateExpression); if (updateDefinition == null) return 0; - - var result = collection.UpdateMany(mongoFilter, updateDefinition); + + var result = GetCollection().UpdateMany(mongoFilter, updateDefinition); return result.ModifiedCount; } public override async Task UpdateManyAsync(Expression> filter, Expression> updateExpression, CancellationToken cancellationToken = default) { - var collection = GetCollection(); - if (collection == null) return 0; - var mongoFilter = Builders.Filter.Where(filter); var updateDefinition = UpdateDefinitionBuilder.BuildUpdateDefinition(updateExpression); if (updateDefinition == null) return 0; - - var result = await collection.UpdateManyAsync(mongoFilter, updateDefinition, null, cancellationToken); + + var result = await GetCollection().UpdateManyAsync(mongoFilter, updateDefinition, null, cancellationToken); return result.ModifiedCount; } @@ -126,9 +122,34 @@ public override IQueryable GetQueryable(Action config return query; } - private IMongoCollection? GetCollection() + private IMongoCollection GetCollection() + { + return GetDatabase().GetCollection(_collectionName); + } + + private IMongoDatabase GetDatabase() { - _mongoDatabase ??= _serviceProvider.GetService()?.GetDatabase(_collectionName); - return _mongoDatabase?.GetCollection(_collectionName); + if (_mongoDatabase != null) + { + return _mongoDatabase; + } + + // The database name comes from the DbContext configuration (UseMongoDB(..., databaseName)), + // NOT from the collection name. Using the collection name here meant every bulk operation ran + // against the wrong database and silently affected zero documents. + if (string.IsNullOrEmpty(_databaseName)) + { + throw new InvalidOperationException( + $"Could not resolve the MongoDB database name for '{typeof(TEntity).Name}'. " + + "Ensure the DbContext is configured with UseMongoDB(..., databaseName)."); + } + + var client = _serviceProvider.GetService() + ?? throw new InvalidOperationException( + "No IMongoClient is registered in the service provider; bulk MongoDB operations " + + "(DeleteMany/UpdateMany) cannot be executed. Register an IMongoClient in DI."); + + _mongoDatabase = client.GetDatabase(_databaseName); + return _mongoDatabase; } } \ No newline at end of file From c2dfad1be672e956c8b88ef9975392c5da33281c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:50:29 +0000 Subject: [PATCH 07/32] =?UTF-8?q?=E2=9A=A1=20fix(core):=20parameterize=20a?= =?UTF-8?q?nd=20cache=20the=20find-by-key=20expression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetFindByKeyExpression embedded the key value as a literal ConstantExpression, so EF Core did not parameterize it: every distinct id produced a new compiled-query cache entry and a SQL literal that polluted the server plan cache (A4). - Reference the key value through a closure holder so EF Core parameterizes it, reproducing the pattern the compiler emits for `x => x.Id == id`. - Cache the primary-key metadata per (context type, entity type) to avoid the model lookup on every call (M7). - Use EF.Property for the entity-side access so shadow keys are supported. - Throw instead of silently building a partial predicate when a composite-key part has no matching property on the key type. Add tests: Get(id, config) exercises the expression path, and the built tree no longer embeds the key value as a literal constant. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Extensions/DbContextExtensions.cs | 114 ++++++++++++++---- .../ReadRepositoryQueryTests.cs | 44 +++++++ 2 files changed, 133 insertions(+), 25 deletions(-) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs index 1812ffa..e37473e 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; @@ -7,10 +8,23 @@ namespace eQuantic.Core.Data.EntityFramework.Repository.Extensions; public static class DbContextExtensions { + private static readonly ConcurrentDictionary<(Type Context, Type Entity), KeyProperty[]> KeyCache = new(); + + /// + /// Builds a predicate that matches an entity by its primary key. + /// + /// + /// The key value is referenced through a closure holder rather than embedded as a literal + /// , so EF Core parameterizes the query. Embedding the value + /// produced a distinct compiled-query cache entry (and a non-parameterized SQL literal) for + /// every id. The primary-key metadata is cached per (context type, entity type) to avoid the + /// model lookup on every call, and is used + /// so shadow keys (without a CLR property) are supported. + /// public static Expression> GetFindByKeyExpression(this DbContext dbContext, TKey id) { - var keyProperties = dbContext.Model.FindEntityType(typeof(TEntity))?.FindPrimaryKey()?.Properties; - if (keyProperties == null || !keyProperties.Any()) + var keyProperties = GetKeyProperties(dbContext); + if (keyProperties.Length == 0) { return null; } @@ -18,41 +32,91 @@ public static Expression> GetFindByKeyExpression>(expression, parameter); } -} \ No newline at end of file + + private static KeyProperty[] GetKeyProperties(DbContext dbContext) + { + return KeyCache.GetOrAdd((dbContext.GetType(), typeof(TEntity)), _ => + { + var properties = dbContext.Model.FindEntityType(typeof(TEntity))?.FindPrimaryKey()?.Properties; + return properties == null + ? Array.Empty() + : properties.Select(p => new KeyProperty(p.Name, p.ClrType)).ToArray(); + }); + } + + private static MethodCallExpression BuildPropertyAccess(ParameterExpression parameter, KeyProperty keyProperty) + { + // EF.Property(entity, name) reads both mapped and shadow properties. + return Expression.Call( + typeof(EF), + nameof(EF.Property), + new[] { keyProperty.ClrType }, + parameter, + Expression.Constant(keyProperty.Name)); + } + + private static Expression BuildKeyValueAccess(TKey id) + { + // Wrapping the value in a holder and reading its field reproduces the closure pattern the C# + // compiler emits for `x => x.Id == id`, which EF Core parameterizes. + var holder = new KeyValueHolder(id); + return Expression.Field(Expression.Constant(holder), nameof(KeyValueHolder.Value)); + } + + private static Expression EnsureType(Expression expression, Type targetType) + { + return expression.Type == targetType ? expression : Expression.Convert(expression, targetType); + } + + private readonly struct KeyProperty + { + public KeyProperty(string name, Type clrType) + { + Name = name; + ClrType = clrType; + } + + public string Name { get; } + public Type ClrType { get; } + } + + private sealed class KeyValueHolder + { + public readonly T Value; + + public KeyValueHolder(T value) + { + Value = value; + } + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs index 1ae3c48..64852de 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.EntityFramework.Repository.Read; using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; using eQuantic.Core.Data.Repository.Config; @@ -104,4 +105,47 @@ public void Get_WithExistingKey_ReturnsEntity() Assert.That(found, Is.Not.Null); Assert.That(found!.Name, Is.EqualTo("found")); } + + [Test] + public void Get_WithConfiguration_UsesKeyExpression_ReturnsEntity() + { + var repository = NewRepository(out var context); + context.Products.Add(new Product { Id = 7, Name = "seven" }); + context.SaveChanges(); + + // A non-null configuration routes Get through GetFindByKeyExpression instead of DbSet.Find. + var found = repository.Get(7, _ => { }); + + Assert.That(found, Is.Not.Null); + Assert.That(found!.Name, Is.EqualTo("seven")); + } + + [Test] + public void GetFindByKeyExpression_DoesNotEmbedKeyValueAsLiteral() + { + _ = NewUnitOfWork(out var context); + + var expression = context.GetFindByKeyExpression(5); + + Assert.That(expression, Is.Not.Null); + var literalFinder = new ConstantValueFinder(5); + literalFinder.Visit(expression); + Assert.That(literalFinder.Found, Is.False, + "The key value must be parameterized (held in a closure), not embedded as a literal constant."); + } + + private sealed class ConstantValueFinder(object target) : ExpressionVisitor + { + public bool Found { get; private set; } + + protected override Expression VisitConstant(ConstantExpression node) + { + if (Equals(node.Value, target)) + { + Found = true; + } + + return base.VisitConstant(node); + } + } } From a18998fa7aca4906ffc19a8b93b80ed0d204b659 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:53:28 +0000 Subject: [PATCH 08/32] =?UTF-8?q?=F0=9F=90=9E=20fix(di):=20honour=20lifeti?= =?UTF-8?q?me,=20dedupe,=20and=20tolerate=20unloadable=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AddRepository hardcoded AddTransient, ignoring the lifetime configured via AddCustomRepositories(o => o.AddLifetime(...)), and registered duplicates when called twice. AddRepositories also called Assembly.GetTypes() directly, which throws ReflectionTypeLoadException at startup when a scanned assembly has an unloadable dependency. - Thread the configured ServiceLifetime through to each repository descriptor and register via TryAdd. - Fall back to the loadable types on ReflectionTypeLoadException instead of failing the whole scan. Add a test asserting the configured lifetime is honoured. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Extensions/ServiceCollectionExtensions.cs | 34 +++++++++++++++---- .../Fakes/FakeRepository.cs | 15 ++++++++ .../ServiceCollectionExtensionsTests.cs | 15 ++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs index 9d0e1f8..fc1ae46 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using eQuantic.Core.Data.EntityFramework.Repository.Options; using eQuantic.Core.Data.Repository; using eQuantic.Core.Data.Repository.Sql; @@ -88,20 +90,21 @@ private static void AddGenericRepositories(IServiceCollection services, ServiceL private static void AddRepositories(IServiceCollection services, RepositoryOptions repoOptions) { + var lifetime = repoOptions.GetLifetime(); var types = repoOptions.GetAssemblies() - .SelectMany(o => o.GetTypes()) + .SelectMany(GetLoadableTypes) .Where(o => o is { IsAbstract: false, IsInterface: false } && o.GetInterfaces().Any(i => i == typeof(IRepository))); foreach (var type in types) { - AddRepository(typeof(IRepository<,,>), type, services); - AddRepository(typeof(IAsyncRepository<,,>), type, services); - AddRepository(typeof(IQueryableRepository<,,>), type, services); - AddRepository(typeof(IAsyncQueryableRepository<,,>), type, services); + AddRepository(typeof(IRepository<,,>), type, services, lifetime); + AddRepository(typeof(IAsyncRepository<,,>), type, services, lifetime); + AddRepository(typeof(IQueryableRepository<,,>), type, services, lifetime); + AddRepository(typeof(IAsyncQueryableRepository<,,>), type, services, lifetime); } } - private static void AddRepository(Type interfaceType, Type type, IServiceCollection services) + private static void AddRepository(Type interfaceType, Type type, IServiceCollection services, ServiceLifetime lifetime) { var interfaces = type.GetInterfaces(); var repoInterface = interfaces.FirstOrDefault(o => @@ -116,7 +119,24 @@ private static void AddRepository(Type interfaceType, Type type, IServiceCollect var entityType = repoInterface.GenericTypeArguments[1]; var keyType = repoInterface.GenericTypeArguments[2]; - services.AddTransient(interfaceType.MakeGenericType(uowType, entityType, keyType), type); + // Honour the configured lifetime instead of forcing Transient, and use TryAdd so calling the + // registration twice does not produce duplicate descriptors. + services.TryAdd(new ServiceDescriptor( + interfaceType.MakeGenericType(uowType, entityType, keyType), type, lifetime)); + } + + private static IEnumerable GetLoadableTypes(Assembly assembly) + { + // Assembly.GetTypes() throws ReflectionTypeLoadException when a dependency cannot be loaded; + // fall back to the types that did load instead of failing the whole registration at startup. + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where(t => t != null)!; + } } private static RepositoryOptions GetOptions(Action options) diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs new file mode 100644 index 0000000..fef26d0 --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs @@ -0,0 +1,15 @@ +using eQuantic.Core.Data.EntityFramework.Repository; + +namespace eQuantic.Core.Data.EntityFramework.Tests.Fakes; + +/// +/// A concrete repository so the assembly-scanning registration (AddCustomRepositories) has +/// something to discover. It inherits the marker IRepository transitively through +/// . +/// +internal sealed class FakeRepository : QueryableRepository +{ + public FakeRepository(FakeQueryableUnitOfWork unitOfWork) : base(unitOfWork) + { + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs index 328dc6b..d07f20a 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs @@ -48,4 +48,19 @@ public void AddQueryableRepositories_RegistersConcreteUnitOfWork_AsQueryableUnit && d.ImplementationType == typeof(FakeQueryableUnitOfWork)), Is.True); } + + [Test] + public void AddCustomRepositories_HonoursConfiguredLifetime() + { + var services = new ServiceCollection(); + + services.AddCustomRepositories(o => o + .AddLifetime(ServiceLifetime.Scoped) + .FromAssembly(typeof(FakeRepository).Assembly)); + + var descriptor = services.FirstOrDefault(d => d.ImplementationType == typeof(FakeRepository)); + Assert.That(descriptor, Is.Not.Null, "The scanned repository should have been registered."); + Assert.That(descriptor!.Lifetime, Is.EqualTo(ServiceLifetime.Scoped), + "AddRepository must honour the configured lifetime instead of forcing Transient."); + } } From 04f42d2819ea6cb02d09b3c7c5a7f3d48cef486e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 16:03:39 +0000 Subject: [PATCH 09/32] =?UTF-8?q?=E2=9C=85=20test(read):=20cover=20GetAllA?= =?UTF-8?q?sync=20and=20document=20the=20load-bearing=20Where?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigating the "redundant" Where(_ => true) in GetAllAsync (plan item M8) showed it is actually required: GetQueryable can return the SetBase wrapper, which does not implement IAsyncEnumerable, and composing a Where turns it into a real EF IQueryable so ToListAsync works. Removing it throws at runtime. Document why it must stay and add a regression test that GetAllAsync returns all entities (which fails if the Where is dropped). Pagination ordering (A5) is deferred: a safe primary-key fallback needs the model at the Set layer. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Read/AsyncQueryableReadRepository.cs | 3 +++ .../ReadRepositoryQueryTests.cs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs index 5fe4e5a..a3e466a 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs @@ -332,6 +332,9 @@ public async Task> GetAllAsync( Action> configuration, CancellationToken cancellationToken) { + // NOTE: the Where(_ => true) is load-bearing, not redundant. GetQueryable can return the + // SetBase wrapper (which does not implement IAsyncEnumerable); composing a Where turns it into + // a real EF IQueryable so ToListAsync works. Removing it breaks the async path. return await GetQueryable(configuration, query => query.Where(_ => true)) .ToListAsync(cancellationToken); } diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs index 64852de..0353a89 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs @@ -46,6 +46,9 @@ private static DefaultUnitOfWork NewUnitOfWork(out TestDbContext context) private static QueryableReadRepository NewRepository(out TestDbContext context) => new(NewUnitOfWork(out context)); + private static AsyncQueryableReadRepository NewAsyncRepository(out TestDbContext context) + => new(NewUnitOfWork(out context)); + [Test] public void All_WithSpecification_HonoursConfiguration() { @@ -134,6 +137,21 @@ public void GetFindByKeyExpression_DoesNotEmbedKeyValueAsLiteral() "The key value must be parameterized (held in a closure), not embedded as a literal constant."); } + [Test] + public async System.Threading.Tasks.Task GetAllAsync_ReturnsAllEntities() + { + var repository = NewAsyncRepository(out var context); + context.Products.AddRange( + new Product { Id = 1, Name = "a" }, + new Product { Id = 2, Name = "b" }, + new Product { Id = 3, Name = "c" }); + context.SaveChanges(); + + var all = await repository.GetAllAsync(System.Threading.CancellationToken.None); + + Assert.That(all.Count(), Is.EqualTo(3)); + } + private sealed class ConstantValueFinder(object target) : ExpressionVisitor { public bool Found { get; private set; } From 30413b00c6ea1394a3a4ad63d2c40f9d1e0d3e0b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 16:08:42 +0000 Subject: [PATCH 10/32] =?UTF-8?q?=E2=9A=A1=20perf(async):=20add=20Configur?= =?UTF-8?q?eAwait(false)=20across=20the=20library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Library code should not capture the caller's SynchronizationContext. Add ConfigureAwait(false) to every awaited operation in the base package and the SqlServer/PostgreSql/MySql/MongoDb providers (91 awaits). `await using` disposals are intentionally left untouched, since ConfigureAwait there would change the declared variable's type. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Repository/Set.cs | 4 +-- .../Repository/UnitOfWork.cs | 8 +++--- .../Repository/Set.cs | 22 ++++++++-------- .../Repository/SqlExecutor.cs | 16 ++++++------ .../Repository/UnitOfWork.cs | 8 +++--- .../Repository/Set.cs | 22 ++++++++-------- .../Repository/SqlExecutor.cs | 16 ++++++------ .../Repository/UnitOfWork.cs | 8 +++--- .../Repository/Set.cs | 26 +++++++++---------- .../Repository/SqlExecutor.cs | 16 ++++++------ .../Repository/UnitOfWork.cs | 8 +++--- .../Read/AsyncQueryableReadRepository.cs | 20 +++++++------- .../Repository/SetBase.cs | 8 +++--- 13 files changed, 91 insertions(+), 91 deletions(-) diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs index 21ffb11..339d1bd 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs @@ -41,7 +41,7 @@ public override long DeleteMany(Expression> filter) public override async Task DeleteManyAsync(Expression> filter, CancellationToken cancellationToken = default) { var mongoFilter = Builders.Filter.Where(filter); - var result = await GetCollection().DeleteManyAsync(mongoFilter, cancellationToken); + var result = await GetCollection().DeleteManyAsync(mongoFilter, cancellationToken).ConfigureAwait(false); return result.DeletedCount; } @@ -66,7 +66,7 @@ public override async Task UpdateManyAsync(Expression> if (updateDefinition == null) return 0; - var result = await GetCollection().UpdateManyAsync(mongoFilter, updateDefinition, null, cancellationToken); + var result = await GetCollection().UpdateManyAsync(mongoFilter, updateDefinition, null, cancellationToken).ConfigureAwait(false); return result.ModifiedCount; } diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs index 15f87d2..d364f68 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs @@ -74,7 +74,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati { try { - changes = await Context.SaveChangesAsync(cancellationToken); + changes = await Context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); saveFailed = false; } @@ -92,7 +92,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati public async Task CommitAsync(CancellationToken cancellationToken = default) { - return await Context.SaveChangesAsync(cancellationToken); + return await Context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } public int Commit(Action options) @@ -145,11 +145,11 @@ public async Task LoadCollectionAsync(TEntity item, { if (filter != null) { - await Context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync(); + await Context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); } else { - await Context.Entry(item).Collection(navigationProperty).LoadAsync(); + await Context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); } } diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs index 2e31064..4cef9e3 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs @@ -28,7 +28,7 @@ public override long DeleteMany(Expression> filter) public override async Task DeleteManyAsync(Expression> filter, CancellationToken cancellationToken = default) { - return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken); + return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); } public void LoadCollection(TChildEntity item, @@ -49,13 +49,13 @@ public async Task LoadCollectionAsync(TChildEnti Expression>> selector) where TChildEntity : class where TComplexProperty : class { - await DbContext.Entry(item).Collection(selector).LoadAsync(); + await DbContext.Entry(item).Collection(selector).LoadAsync().ConfigureAwait(false); } public async Task LoadCollectionAsync(TChildEntity item, string propertyName) where TChildEntity : class { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(); + await DbContext.Entry(item).Collection(propertyName).LoadAsync().ConfigureAwait(false); } public void LoadProperties(TEntity entity, params string[] properties) @@ -100,11 +100,11 @@ public async Task LoadPropertiesAsync(TEntity entity, params string[] properties if (props.Length == 1) { - await LoadPropertyAsync(entity, property); + await LoadPropertyAsync(entity, property).ConfigureAwait(false); } else { - await LoadCascadeAsync(props, entity); + await LoadCascadeAsync(props, entity).ConfigureAwait(false); } } } @@ -135,7 +135,7 @@ public async Task LoadPropertyAsync(TChildEntity Expression> selector, CancellationToken cancellationToken = default) where TChildEntity : class where TComplexProperty : class { - await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken); + await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken).ConfigureAwait(false); } public async Task LoadPropertyAsync(TChildEntity item, string propertyName, @@ -144,11 +144,11 @@ public async Task LoadPropertyAsync(TChildEntity item, string prop { if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken); + await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); } else { - await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken); + await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); } } @@ -163,7 +163,7 @@ public override async Task UpdateManyAsync(Expression> Expression> updateExpression, CancellationToken cancellationToken = default) { var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken); + return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken).ConfigureAwait(false); } private void LoadCascade(string[] props, object obj, int index = 0) @@ -198,13 +198,13 @@ private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) var nextObj = prop?.GetValue(obj); if (nextObj == null) { - await LoadPropertyAsync(obj, props[index]); + await LoadPropertyAsync(obj, props[index]).ConfigureAwait(false); nextObj = prop?.GetValue(obj); } if (props.Length > index + 1) { - await LoadCascadeAsync(props, nextObj, index + 1); + await LoadCascadeAsync(props, nextObj, index + 1).ConfigureAwait(false); } } diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs index dd04cba..478a9f3 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs @@ -55,7 +55,7 @@ protected SqlExecutor(DbContext context) public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { Transaction?.Dispose(); - Transaction = await Context.Database.BeginTransactionAsync(cancellationToken); + Transaction = await Context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); } /// @@ -66,7 +66,7 @@ public virtual async Task CommitTransactionAsync(CancellationToken cancellationT { if (Transaction != null) { - await Transaction.CommitAsync(cancellationToken); + await Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); } } @@ -78,7 +78,7 @@ public async Task RollbackTransactionAsync(CancellationToken cancellationToken = { if (Transaction != null) { - await Transaction.RollbackAsync(cancellationToken); + await Transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); } } @@ -91,11 +91,11 @@ public async Task> ExecuteRawSqlAsync(string sql, Func(); - while (await result.ReadAsync(cancellationToken)) + while (await result.ReadAsync(cancellationToken).ConfigureAwait(false)) { items.Add(map(result)); } @@ -169,8 +169,8 @@ public async Task ExecuteCommandAsync(string sqlCommand, await using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sqlCommand, command, configuration); - await Context.Database.OpenConnectionAsync(cancellationToken); - var result = await command.ExecuteScalarAsync(cancellationToken); + await Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); + var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); return result == DBNull.Value ? 0 : Convert.ToInt32(result); } diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs index 24c58d4..47b7968 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs @@ -74,7 +74,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati { try { - changes = await _context.SaveChangesAsync(cancellationToken); + changes = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); saveFailed = false; } @@ -92,7 +92,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati public async Task CommitAsync(CancellationToken cancellationToken = default) { - return await _context.SaveChangesAsync(cancellationToken); + return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } public int Commit(Action options) @@ -178,11 +178,11 @@ public async Task LoadCollectionAsync(TEntity item, { if (filter != null) { - await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync(); + await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); } else { - await _context.Entry(item).Collection(navigationProperty).LoadAsync(); + await _context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); } } diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs index 2c60b1d..562fabb 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs @@ -28,7 +28,7 @@ public override long DeleteMany(Expression> filter) public override async Task DeleteManyAsync(Expression> filter, CancellationToken cancellationToken = default) { - return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken); + return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); } public void LoadCollection(TChildEntity item, @@ -49,13 +49,13 @@ public async Task LoadCollectionAsync(TChildEnti Expression>> selector) where TChildEntity : class where TComplexProperty : class { - await DbContext.Entry(item).Collection(selector).LoadAsync(); + await DbContext.Entry(item).Collection(selector).LoadAsync().ConfigureAwait(false); } public async Task LoadCollectionAsync(TChildEntity item, string propertyName) where TChildEntity : class { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(); + await DbContext.Entry(item).Collection(propertyName).LoadAsync().ConfigureAwait(false); } public void LoadProperties(TEntity entity, params string[] properties) @@ -100,11 +100,11 @@ public async Task LoadPropertiesAsync(TEntity entity, params string[] properties if (props.Length == 1) { - await LoadPropertyAsync(entity, property); + await LoadPropertyAsync(entity, property).ConfigureAwait(false); } else { - await LoadCascadeAsync(props, entity); + await LoadCascadeAsync(props, entity).ConfigureAwait(false); } } } @@ -135,7 +135,7 @@ public async Task LoadPropertyAsync(TChildEntity Expression> selector, CancellationToken cancellationToken = default) where TChildEntity : class where TComplexProperty : class { - await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken); + await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken).ConfigureAwait(false); } public async Task LoadPropertyAsync(TChildEntity item, string propertyName, @@ -144,11 +144,11 @@ public async Task LoadPropertyAsync(TChildEntity item, string prop { if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken); + await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); } else { - await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken); + await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); } } @@ -163,7 +163,7 @@ public override async Task UpdateManyAsync(Expression> Expression> updateExpression, CancellationToken cancellationToken = default) { var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken); + return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken).ConfigureAwait(false); } private void LoadCascade(string[] props, object obj, int index = 0) @@ -198,13 +198,13 @@ private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) var nextObj = prop?.GetValue(obj); if (nextObj == null) { - await LoadPropertyAsync(obj, props[index]); + await LoadPropertyAsync(obj, props[index]).ConfigureAwait(false); nextObj = prop?.GetValue(obj); } if (props.Length > index + 1) { - await LoadCascadeAsync(props, nextObj, index + 1); + await LoadCascadeAsync(props, nextObj, index + 1).ConfigureAwait(false); } } diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs index 725d750..4a919ba 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs @@ -55,7 +55,7 @@ protected SqlExecutor(DbContext context) public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { Transaction?.Dispose(); - Transaction = await Context.Database.BeginTransactionAsync(cancellationToken); + Transaction = await Context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); } /// @@ -66,7 +66,7 @@ public virtual async Task CommitTransactionAsync(CancellationToken cancellationT { if (Transaction != null) { - await Transaction.CommitAsync(cancellationToken); + await Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); } } @@ -78,7 +78,7 @@ public async Task RollbackTransactionAsync(CancellationToken cancellationToken = { if (Transaction != null) { - await Transaction.RollbackAsync(cancellationToken); + await Transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); } } @@ -91,11 +91,11 @@ public async Task> ExecuteRawSqlAsync(string sql, Func(); - while (await result.ReadAsync(cancellationToken)) + while (await result.ReadAsync(cancellationToken).ConfigureAwait(false)) { items.Add(map(result)); } @@ -169,8 +169,8 @@ public async Task ExecuteCommandAsync(string sqlCommand, await using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sqlCommand, command, configuration); - await Context.Database.OpenConnectionAsync(cancellationToken); - var result = await command.ExecuteScalarAsync(cancellationToken); + await Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); + var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); return result == DBNull.Value ? 0 : Convert.ToInt32(result); } diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs index 3e03f66..d704c47 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs @@ -74,7 +74,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati { try { - changes = await _context.SaveChangesAsync(cancellationToken); + changes = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); saveFailed = false; } @@ -92,7 +92,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati public async Task CommitAsync(CancellationToken cancellationToken = default) { - return await _context.SaveChangesAsync(cancellationToken); + return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } public int Commit(Action options) @@ -178,11 +178,11 @@ public async Task LoadCollectionAsync(TEntity item, { if (filter != null) { - await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync(); + await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); } else { - await _context.Entry(item).Collection(navigationProperty).LoadAsync(); + await _context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); } } diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs index aeb413f..b5eaf8a 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs @@ -36,9 +36,9 @@ public override async Task DeleteManyAsync(Expression> CancellationToken cancellationToken = default) { #if NET6_0 || NETSTANDARD2_1 - return await InternalDbSet.Where(filter).DeleteAsync(cancellationToken); + return await InternalDbSet.Where(filter).DeleteAsync(cancellationToken).ConfigureAwait(false); #else - return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken); + return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); #endif } @@ -60,13 +60,13 @@ public async Task LoadCollectionAsync(TChildEnti Expression>> selector) where TChildEntity : class where TComplexProperty : class { - await DbContext.Entry(item).Collection(selector).LoadAsync(); + await DbContext.Entry(item).Collection(selector).LoadAsync().ConfigureAwait(false); } public async Task LoadCollectionAsync(TChildEntity item, string propertyName) where TChildEntity : class { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(); + await DbContext.Entry(item).Collection(propertyName).LoadAsync().ConfigureAwait(false); } public void LoadProperties(TEntity entity, params string[] properties) @@ -111,11 +111,11 @@ public async Task LoadPropertiesAsync(TEntity entity, params string[] properties if (props.Length == 1) { - await LoadPropertyAsync(entity, property); + await LoadPropertyAsync(entity, property).ConfigureAwait(false); } else { - await LoadCascadeAsync(props, entity); + await LoadCascadeAsync(props, entity).ConfigureAwait(false); } } } @@ -146,7 +146,7 @@ public async Task LoadPropertyAsync(TChildEntity Expression> selector, CancellationToken cancellationToken = default) where TChildEntity : class where TComplexProperty : class { - await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken); + await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken).ConfigureAwait(false); } public async Task LoadPropertyAsync(TChildEntity item, string propertyName, @@ -155,11 +155,11 @@ public async Task LoadPropertyAsync(TChildEntity item, string prop { if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken); + await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); } else { - await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken); + await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); } } @@ -178,10 +178,10 @@ public override async Task UpdateManyAsync(Expression> Expression> updateExpression, CancellationToken cancellationToken = default) { #if NET6_0 || NETSTANDARD2_1 - return await InternalDbSet.Where(filter).UpdateAsync(updateExpression, cancellationToken); + return await InternalDbSet.Where(filter).UpdateAsync(updateExpression, cancellationToken).ConfigureAwait(false); #else var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken); + return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken).ConfigureAwait(false); #endif } @@ -217,13 +217,13 @@ private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) var nextObj = prop?.GetValue(obj); if (nextObj == null) { - await LoadPropertyAsync(obj, props[index]); + await LoadPropertyAsync(obj, props[index]).ConfigureAwait(false); nextObj = prop?.GetValue(obj); } if (props.Length > index + 1) { - await LoadCascadeAsync(props, nextObj, index + 1); + await LoadCascadeAsync(props, nextObj, index + 1).ConfigureAwait(false); } } diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs index bc77e31..eba162f 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs @@ -55,7 +55,7 @@ protected SqlExecutor(DbContext context) public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { Transaction?.Dispose(); - Transaction = await Context.Database.BeginTransactionAsync(cancellationToken); + Transaction = await Context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); } /// @@ -66,7 +66,7 @@ public virtual async Task CommitTransactionAsync(CancellationToken cancellationT { if (Transaction != null) { - await Transaction.CommitAsync(cancellationToken); + await Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); } } @@ -78,7 +78,7 @@ public async Task RollbackTransactionAsync(CancellationToken cancellationToken = { if (Transaction != null) { - await Transaction.RollbackAsync(cancellationToken); + await Transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); } } @@ -91,11 +91,11 @@ public async Task> ExecuteRawSqlAsync(string sql, Func(); - while (await result.ReadAsync(cancellationToken)) + while (await result.ReadAsync(cancellationToken).ConfigureAwait(false)) { items.Add(map(result)); } @@ -169,8 +169,8 @@ public async Task ExecuteCommandAsync(string sqlCommand, await using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sqlCommand, command, configuration); - await Context.Database.OpenConnectionAsync(cancellationToken); - var result = await command.ExecuteScalarAsync(cancellationToken); + await Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); + var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); return result == DBNull.Value ? 0 : Convert.ToInt32(result); } diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs index fcfa394..8dfefeb 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs @@ -74,7 +74,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati { try { - changes = await _context.SaveChangesAsync(cancellationToken); + changes = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); saveFailed = false; } @@ -92,7 +92,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati public async Task CommitAsync(CancellationToken cancellationToken = default) { - return await _context.SaveChangesAsync(cancellationToken); + return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } public int Commit(Action options) @@ -178,11 +178,11 @@ public async Task LoadCollectionAsync(TEntity item, { if (filter != null) { - await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync(); + await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); } else { - await _context.Entry(item).Collection(navigationProperty).LoadAsync(); + await _context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); } } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs index a3e466a..311d205 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs @@ -41,7 +41,7 @@ public async Task> AllMatchingAsync( return await GetQueryable(configuration, query => query .Where(specification.SatisfiedBy())) - .ToListAsync(cancellationToken); + .ToListAsync(cancellationToken).ConfigureAwait(false); } public Task> AllMatchingAsync( @@ -336,7 +336,7 @@ public async Task> GetAllAsync( // SetBase wrapper (which does not implement IAsyncEnumerable); composing a Where turns it into // a real EF IQueryable so ToListAsync works. Removing it breaks the async path. return await GetQueryable(configuration, query => query.Where(_ => true)) - .ToListAsync(cancellationToken); + .ToListAsync(cancellationToken).ConfigureAwait(false); } public Task> GetAllAsync( @@ -388,7 +388,7 @@ public async Task> GetMappedAsync( { return await GetQueryable(configuration, query => query.Where(filter)) .Select(map) - .ToListAsync(cancellationToken); + .ToListAsync(cancellationToken).ConfigureAwait(false); } public Task> GetMappedAsync( @@ -442,7 +442,7 @@ public async Task> GetFilteredAsync( CancellationToken cancellationToken) { return await GetQueryable(configuration, query => query.Where(filter)) - .ToListAsync(cancellationToken); + .ToListAsync(cancellationToken).ConfigureAwait(false); } public Task> GetFilteredAsync( @@ -465,7 +465,7 @@ public async Task GetFirstAsync( CancellationToken cancellationToken) { return await GetQueryable(configuration, query => query.Where(filter)) - .FirstOrDefaultAsync(cancellationToken); + .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } public Task GetFirstAsync( @@ -732,10 +732,10 @@ public async Task> GetPagedAsync( if (pageIndex < 1) pageIndex = 1; if (pageSize > 0) { - return await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken); + return await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false); } - return await query.ToListAsync(cancellationToken); + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); } public Task> GetPagedAsync( @@ -760,7 +760,7 @@ public async Task GetSingleAsync( CancellationToken cancellationToken) { return await GetQueryable(configuration, query => query.Where(filter)) - .SingleOrDefaultAsync(cancellationToken); + .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); } public Task GetSingleAsync( @@ -802,12 +802,12 @@ private async Task GetInternalAsync(TKey id, Action query.Where(idExpression)) - .SingleOrDefaultAsync(cancellationToken); + .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); } private IQueryable GetQueryable(Action> configuration, diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs index 7e59982..f75f0da 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs @@ -97,17 +97,17 @@ public virtual TEntity Find(TKey key) public virtual async Task FindAsync(object[] keyValues, CancellationToken cancellationToken) { - return await InternalDbSet.FindAsync(keyValues, cancellationToken); + return await InternalDbSet.FindAsync(keyValues, cancellationToken).ConfigureAwait(false); } public virtual async Task FindAsync(params object[] keyValues) { - return await InternalDbSet.FindAsync(keyValues); + return await InternalDbSet.FindAsync(keyValues).ConfigureAwait(false); } public virtual async Task FindAsync(TKey key, CancellationToken cancellationToken = default) { - return await InternalDbSet.FindAsync(new object[] { key }, cancellationToken); + return await InternalDbSet.FindAsync(new object[] { key }, cancellationToken).ConfigureAwait(false); } public virtual IEnumerator GetEnumerator() @@ -132,7 +132,7 @@ public virtual void Insert(TEntity item) public virtual async Task InsertAsync(TEntity item, CancellationToken cancellationToken = default) { - await InternalDbSet.AddAsync(item, cancellationToken); + await InternalDbSet.AddAsync(item, cancellationToken).ConfigureAwait(false); } public virtual EntityEntry Remove(TEntity entity) From 41ad0674ecb479a6d5868e69b9235552d0c9f730 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 16:09:29 +0000 Subject: [PATCH 11/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20update=20Phase=201?= =?UTF-8?q?=20status=20(P1/P6,=20A4/M7,=20M4,=20ConfigureAwait;=20M8=20cor?= =?UTF-8?q?rected)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- docs/IMPROVEMENT_PLAN.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md index c8b3ee9..72c87dd 100644 --- a/docs/IMPROVEMENT_PLAN.md +++ b/docs/IMPROVEMENT_PLAN.md @@ -31,9 +31,10 @@ O plano abaixo está em **5 fases**. As Fases 0–2 **não quebram contrato** e As Fases 3–4 definem a **v5.0.0 dos contratos** (breaking deliberado) e a estratégia de versionamento no nuget.org. -## Status de implementação (Fase 1 iniciada) +## Status de implementação (Fase 1 — grande parte concluída) -Correções já aplicadas nesta branch, com testes (17 testes passando, todos os 5 pacotes compilando em net10): +Correções já aplicadas nesta branch, **com testes** (21 testes passando; todos os 5 pacotes compilando — +base/SqlServer/PostgreSql/MySql em net10, MongoDb em net8): | Achado | Correção | Commit | |--------|----------|--------| @@ -42,11 +43,26 @@ Correções já aplicadas nesta branch, com testes (17 testes passando, todos os | **C1** double-dispose | `_disposed` unificado (`protected`); UoW disposto exatamente uma vez. | `🐞 fix(core)` | | **A1** config descartada | `All`/`Any` sync repassam `configuration`. | `🐞 fix(read)` | | **A2** chave default | `Get`/`GetAsync` validam `id is null` em vez de `default(TKey)`. | `🐞 fix(read)` | - -Substituído o teste placebo (`Assert.Pass()`) por cobertura real (DI, disposal, parametrização de SQL, -queries via EF InMemory). **Pendente na Fase 1** (próximos passos): C2 (ownership do UoW injetado — -comportamental, precisa de decisão), P1/P2 (MongoDb banco errado / update de constante), M-core -(`ConfigureAwait(false)`, cache de expressão de chave, `AddRepository` respeitando lifetime), A4/A5. +| **P1/P6** MongoDb | Usa o banco configurado no `DbContext` (não o nome da coleção); lança em vez de retornar `0` silencioso. | `🐞 fix(mongodb)` | +| **A4/M7** chave | Valor da chave parametrizado (closure) em vez de literal; metadados de PK cacheados; `EF.Property` para shadow keys; lança em chave composta parcial. | `⚡ fix(core)` | +| **M4** DI | `AddRepository` respeita o lifetime configurado, usa `TryAdd`, e tolera `ReflectionTypeLoadException`. | `🐞 fix(di)` | +| **M2** async | `ConfigureAwait(false)` em 91 awaits de biblioteca (base + 4 providers). | `⚡ perf(async)` | + +Substituído o teste placebo (`Assert.Pass()`) por cobertura real: DI, disposal, parametrização de SQL, +queries e expressão de chave via EF InMemory. + +**Investigado e corrigido no diagnóstico:** o item **M8** do plano (remover `Where(_ => true)` em +`GetAllAsync`) estava **errado** — esse `Where` é load-bearing (o `SetBase` não implementa +`IAsyncEnumerable`; o `Where` o converte num `IQueryable` real do EF). Documentado no código + teste de +regressão; nada removido. + +**Pendente na Fase 1 (precisa de decisão/design, deixado de fora deste lote):** +- **C2** — ownership do `UnitOfWork` injetado: hoje o repositório dispõe o UoW recebido por DI. Corrigir + é a coisa certa, mas muda comportamento observável — decisão de produto. +- **P2** — MongoDb `UpdateDefinitionBuilder` grava constante (`x => x.Count + 1` vira `1`): a correção é + **rejeitar** (lançar) ou **traduzir para `$inc`** — esforços bem diferentes. +- **A5** — paginação sem `OrderBy`: um fallback seguro por PK precisa do model na camada do `Set` (4 + providers), não é one-liner no repo base. --- From 8fde11673af1706b5b94f95b69c52adaae2c611e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 10:12:43 +0000 Subject: [PATCH 12/32] =?UTF-8?q?=F0=9F=90=9E=20fix(mongodb):=20reject=20u?= =?UTF-8?q?pdate=20expressions=20that=20reference=20the=20entity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UpdateDefinitionBuilder compiled each member assignment and invoked it against Activator.CreateInstance (a default instance), so an expression like x => new E { Count = x.Count + 1 } silently wrote the constant 1 to every matched document instead of incrementing — silent data corruption. Detect references to the update parameter with an ExpressionVisitor and throw NotSupportedException with a clear message. Constant and captured (closure) values keep working. Translating entity-referencing expressions to $inc/pipeline updates can be layered on later; corrupting data silently is never acceptable in the meantime. Add a MongoDb test project (net8, matching the package TFM) covering the constant, captured and entity-referencing cases. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../UpdateDefinitionBuilder.cs | 42 ++++++++++++++++ .../UpdateDefinitionBuilderTests.cs | 48 +++++++++++++++++++ ....Data.EntityFramework.MongoDb.Tests.csproj | 27 +++++++++++ 3 files changed, 117 insertions(+) create mode 100644 tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/UpdateDefinitionBuilderTests.cs create mode 100644 tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/UpdateDefinitionBuilder.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/UpdateDefinitionBuilder.cs index 1a6a34e..364dc0f 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/UpdateDefinitionBuilder.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/UpdateDefinitionBuilder.cs @@ -37,11 +37,53 @@ internal static class UpdateDefinitionBuilder private static object? GetValueFromExpression(Expression expression, IReadOnlyList parameters) { + // The value is evaluated against a fresh default instance of the entity. Any assignment that + // reads the entity (e.g. x => x.Count + 1) would therefore be evaluated against Count == 0 and + // silently write the constant 1 to every matched document instead of incrementing it. Reject + // such expressions loudly rather than corrupting data; only constant/closure values are safe. + if (ReferencesParameter(expression, parameters)) + { + throw new NotSupportedException( + "The MongoDB update builder does not support update expressions that reference the " + + "entity being updated (e.g. x => x.Count + 1). Such an expression would be evaluated " + + "against a default instance and would overwrite documents with a constant value. Use a " + + "constant or captured value instead."); + } + var lambda = Expression.Lambda(expression, parameters); var compiledLambda = lambda.Compile(); return compiledLambda.DynamicInvoke(Activator.CreateInstance(parameters[0].Type)); } + private static bool ReferencesParameter(Expression expression, IReadOnlyList parameters) + { + var detector = new ParameterReferenceDetector(parameters); + detector.Visit(expression); + return detector.Found; + } + + private sealed class ParameterReferenceDetector : ExpressionVisitor + { + private readonly IReadOnlyList _parameters; + + public ParameterReferenceDetector(IReadOnlyList parameters) + { + _parameters = parameters; + } + + public bool Found { get; private set; } + + protected override Expression VisitParameter(ParameterExpression node) + { + if (_parameters.Contains(node)) + { + Found = true; + } + + return base.VisitParameter(node); + } + } + private static bool IsSimpleType(Type type) { return type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime); diff --git a/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/UpdateDefinitionBuilderTests.cs b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/UpdateDefinitionBuilderTests.cs new file mode 100644 index 0000000..a2a5591 --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/UpdateDefinitionBuilderTests.cs @@ -0,0 +1,48 @@ +using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.MongoDb; +using eQuantic.Core.Data.Repository; + +namespace eQuantic.Core.Data.EntityFramework.MongoDb.Tests; + +/// +/// Guards the fix for the silent data-corruption defect: an update expression that reads the entity +/// (e.g. x => x.Count + 1) was evaluated against a default instance and wrote a constant. It must now +/// be rejected, while constant/closure values keep working. +/// +public class UpdateDefinitionBuilderTests +{ + private sealed class Doc : IEntity + { + public int Count { get; set; } + public string Name { get; set; } = string.Empty; + } + + [Test] + public void BuildUpdateDefinition_ConstantValue_BuildsDefinition() + { + Expression> update = _ => new Doc { Name = "fixed" }; + + var definition = UpdateDefinitionBuilder.BuildUpdateDefinition(update); + + Assert.That(definition, Is.Not.Null); + } + + [Test] + public void BuildUpdateDefinition_CapturedValue_BuildsDefinition() + { + var captured = 42; + Expression> update = _ => new Doc { Count = captured }; + + var definition = UpdateDefinitionBuilder.BuildUpdateDefinition(update); + + Assert.That(definition, Is.Not.Null); + } + + [Test] + public void BuildUpdateDefinition_ExpressionReferencingEntity_Throws() + { + Expression> update = d => new Doc { Count = d.Count + 1 }; + + Assert.Throws(() => UpdateDefinitionBuilder.BuildUpdateDefinition(update)); + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj new file mode 100644 index 0000000..d9f088c --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + From 89c3bacde33d954a671eadc1e55c5094cf15bbc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 10:16:38 +0000 Subject: [PATCH 13/32] =?UTF-8?q?=F0=9F=90=9E=20fix(core):=20stop=20dispos?= =?UTF-8?q?ing=20the=20injected=20UnitOfWork=20(ownership)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BEHAVIORAL CHANGE. The repository always receives its UnitOfWork by constructor injection and never creates it, so it must not dispose it — the creator owns the lifetime. Previously the composite repository disposed the UnitOfWork unconditionally and the standalone read/write repositories did so via OwnUnitOfWork (defaulting to true). Under DI, where AddGenericRepositories registers the UnitOfWork and the repositories together, this disposed the shared DbContext out from under the other repositories in the scope and double-disposed it alongside the container. - Remove the UnitOfWork disposal from the composite QueryableRepository. - Default OwnUnitOfWork to false in QueryableReadRepository/WriteRepository. Callers that previously relied on disposing a repository to close a manually created context must now dispose the UnitOfWork (or DbContext) themselves. Update the disposal tests to assert the injected UnitOfWork is not disposed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Repository/QueryableRepository.cs | 5 +++- .../Read/QueryableReadRepository.cs | 8 +++++- .../Repository/Write/WriteRepository.cs | 8 +++++- .../RepositoryDisposalTests.cs | 25 ++++++++++++++----- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs index 1c1f979..287b970 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs @@ -377,7 +377,10 @@ protected virtual void Dispose(bool disposing) { this._readRepository?.Dispose(); this._writeRepository?.Dispose(); - UnitOfWork?.Dispose(); + // The UnitOfWork is injected, not created here, so its creator owns its lifetime — the DI + // container (which registers the UoW and the repository together) or the caller that built + // it. Disposing it here disposed the shared DbContext out from under the other repositories + // in the same scope and double-disposed it alongside the container. } Disposed = true; diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs index a34c02c..b98202f 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs @@ -19,7 +19,13 @@ public class QueryableReadRepository : { internal SetBase _dbSet; private bool _disposed; - internal bool OwnUnitOfWork { get; set; } = true; + + /// + /// Whether this repository owns the injected 's lifetime. Defaults to + /// false: the UnitOfWork is provided by its creator (the DI container or the caller), and + /// disposing the repository must not dispose a UnitOfWork it did not create. + /// + internal bool OwnUnitOfWork { get; set; } = false; private const string SpecificationCannotBeNull = "Specification cannot be null"; private const string FilterExpressionCannotBeNull = "Filter expression cannot be null"; /// diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs index 2aeca70..876b712 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs @@ -12,7 +12,13 @@ public class WriteRepository : IWriteRepository _dbSet; private bool _disposed; - internal bool OwnUnitOfWork { get; set; } = true; + + /// + /// Whether this repository owns the injected 's lifetime. Defaults to + /// false: the UnitOfWork is provided by its creator (the DI container or the caller), and + /// disposing the repository must not dispose a UnitOfWork it did not create. + /// + internal bool OwnUnitOfWork { get; set; } = false; public WriteRepository(TUnitOfWork unitOfWork) { diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs index d12a851..9066131 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs @@ -4,21 +4,22 @@ namespace eQuantic.Core.Data.EntityFramework.Tests; /// -/// Guards the fix for the double-dispose defect: disposing an -/// must dispose its unit of work exactly once, not twice (the shadowed disposal flag made both the -/// base and derived disposal blocks run). +/// Covers unit-of-work ownership on disposal. The repository receives its UnitOfWork by +/// injection, so it must NOT dispose it (its creator — the DI container or the caller — owns the +/// lifetime). This also removes the previous double-dispose of the shared DbContext. /// public class RepositoryDisposalTests { [Test] - public void Dispose_AsyncQueryableRepository_DisposesUnitOfWork_Once() + public void Dispose_AsyncQueryableRepository_DoesNotDisposeInjectedUnitOfWork() { var unitOfWork = new FakeQueryableUnitOfWork(); var repository = new AsyncQueryableRepository(unitOfWork); repository.Dispose(); - Assert.That(unitOfWork.DisposeCount, Is.EqualTo(1)); + Assert.That(unitOfWork.DisposeCount, Is.EqualTo(0), + "The repository must not dispose a UnitOfWork it did not create."); } [Test] @@ -30,6 +31,18 @@ public void Dispose_AsyncQueryableRepository_IsIdempotent() repository.Dispose(); repository.Dispose(); - Assert.That(unitOfWork.DisposeCount, Is.EqualTo(1)); + Assert.That(unitOfWork.DisposeCount, Is.EqualTo(0)); + } + + [Test] + public void Dispose_QueryableReadRepository_DoesNotDisposeInjectedUnitOfWork() + { + var unitOfWork = new FakeQueryableUnitOfWork(); + var repository = new eQuantic.Core.Data.EntityFramework.Repository.Read + .QueryableReadRepository(unitOfWork); + + repository.Dispose(); + + Assert.That(unitOfWork.DisposeCount, Is.EqualTo(0)); } } From 6c416fe16d49de0b2293889c957984c8b30df6bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 10:22:19 +0000 Subject: [PATCH 14/32] =?UTF-8?q?=F0=9F=90=9E=20fix(read):=20deterministic?= =?UTF-8?q?=20pagination=20via=20primary-key=20fallback=20ordering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetPaged/GetPagedAsync applied Skip/Take without guaranteeing an OrderBy, so without explicit sorting the pages were non-deterministic (rows could repeat or vanish between pages) and EF Core warned about it. Add DbContext.OrderByPrimaryKeyIfUnordered: if the query is already ordered (detected by walking the expression tree for Queryable ordering calls) it is returned unchanged; otherwise it is ordered by the primary key (composite keys via OrderBy + ThenBy, using the cached key metadata and EF.Property). Apply it in both pagination paths only when actually paging. Tests: unsorted pagination now orders by key, explicit ordering is preserved, and the helper is a no-op on an already-ordered query. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .../Extensions/DbContextExtensions.cs | 67 +++++++++++++++++++ .../Read/AsyncQueryableReadRepository.cs | 2 + .../Read/QueryableReadRepository.cs | 9 ++- .../ReadRepositoryQueryTests.cs | 45 +++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs index e37473e..9c40125 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; @@ -63,6 +64,51 @@ public static Expression> GetFindByKeyExpression>(expression, parameter); } + /// + /// Ensures a deterministic order before paging. If the query is already ordered, it is returned + /// unchanged; otherwise it is ordered by the primary key. Skip/Take without an OrderBy produces + /// non-deterministic pages (rows may repeat or vanish between pages) and warns in EF Core. + /// + public static IQueryable OrderByPrimaryKeyIfUnordered(this IQueryable query, DbContext dbContext) + where TEntity : class + { + if (IsOrdered(query.Expression)) + { + return query; + } + + var keyProperties = GetKeyProperties(dbContext); + if (keyProperties.Length == 0) + { + // No primary key to fall back on (e.g. a keyless entity); leave ordering to the caller. + return query; + } + + var ordered = query; + for (var i = 0; i < keyProperties.Length; i++) + { + var keyProperty = keyProperties[i]; + var parameter = Expression.Parameter(typeof(TEntity), "entity"); + var access = BuildPropertyAccess(parameter, keyProperty); + var selector = Expression.Lambda(access, parameter); + + var methodName = i == 0 ? nameof(Queryable.OrderBy) : nameof(Queryable.ThenBy); + var method = typeof(Queryable).GetMethods() + .First(m => m.Name == methodName && m.GetParameters().Length == 2) + .MakeGenericMethod(typeof(TEntity), keyProperty.ClrType); + ordered = (IQueryable)method.Invoke(null, new object[] { ordered, selector })!; + } + + return ordered; + } + + private static bool IsOrdered(Expression expression) + { + var detector = new OrderingDetector(); + detector.Visit(expression); + return detector.Found; + } + private static KeyProperty[] GetKeyProperties(DbContext dbContext) { return KeyCache.GetOrAdd((dbContext.GetType(), typeof(TEntity)), _ => @@ -98,6 +144,27 @@ private static Expression EnsureType(Expression expression, Type targetType) return expression.Type == targetType ? expression : Expression.Convert(expression, targetType); } + private sealed class OrderingDetector : ExpressionVisitor + { + private static readonly HashSet OrderingMethods = new() + { + nameof(Queryable.OrderBy), nameof(Queryable.OrderByDescending), + nameof(Queryable.ThenBy), nameof(Queryable.ThenByDescending) + }; + + public bool Found { get; private set; } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(Queryable) && OrderingMethods.Contains(node.Method.Name)) + { + Found = true; + } + + return base.VisitMethodCall(node); + } + } + private readonly struct KeyProperty { public KeyProperty(string name, Type clrType) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs index 311d205..6a780f2 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs @@ -5,6 +5,7 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; using eQuantic.Core.Data.Repository.Config; using eQuantic.Core.Data.Repository.Read; @@ -732,6 +733,7 @@ public async Task> GetPagedAsync( if (pageIndex < 1) pageIndex = 1; if (pageSize > 0) { + query = query.OrderByPrimaryKeyIfUnordered(GetSet().DbContext); return await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs index b98202f..59591ba 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; using eQuantic.Core.Data.Repository.Config; using eQuantic.Core.Data.Repository.Read; @@ -399,7 +400,13 @@ public IEnumerable GetPaged(Expression> filter, int return internalQuery; }); if (pageIndex < 1) pageIndex = 1; - return pageSize > 0 ? query.Skip((pageIndex - 1) * pageSize).Take(pageSize) : query; + if (pageSize <= 0) + { + return query; + } + + query = query.OrderByPrimaryKeyIfUnordered(GetSet().DbContext); + return query.Skip((pageIndex - 1) * pageSize).Take(pageSize); } public TEntity GetSingle(Expression> filter, Action> configuration = default) diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs index 0353a89..1108576 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs @@ -152,6 +152,51 @@ public async System.Threading.Tasks.Task GetAllAsync_ReturnsAllEntities() Assert.That(all.Count(), Is.EqualTo(3)); } + [Test] + public void GetPaged_WithoutSorting_OrdersByPrimaryKeyDeterministically() + { + var repository = NewRepository(out var context); + // Insert out of key order; without a fallback OrderBy the page order would be undefined. + context.Products.AddRange( + new Product { Id = 3, Name = "c" }, + new Product { Id = 1, Name = "a" }, + new Product { Id = 2, Name = "b" }); + context.SaveChanges(); + + var firstPage = repository.GetPaged(p => true, 1, 2, null).ToList(); + + Assert.That(firstPage.Select(p => p.Id), Is.EqualTo(new[] { 1, 2 })); + } + + [Test] + public void GetPaged_WithExplicitOrdering_IsPreserved() + { + var repository = NewRepository(out var context); + context.Products.AddRange( + new Product { Id = 1, Name = "a" }, + new Product { Id = 2, Name = "b" }, + new Product { Id = 3, Name = "c" }); + context.SaveChanges(); + + // Caller orders descending; the primary-key fallback must NOT override it. + var firstPage = repository + .GetPaged(p => true, 1, 2, cfg => cfg.WithAfterCustomization(q => q.OrderByDescending(p => p.Id))) + .ToList(); + + Assert.That(firstPage.Select(p => p.Id), Is.EqualTo(new[] { 3, 2 })); + } + + [Test] + public void OrderByPrimaryKeyIfUnordered_AlreadyOrdered_ReturnsSameQuery() + { + _ = NewUnitOfWork(out var context); + var ordered = context.Set().OrderByDescending(p => p.Name); + + var result = ordered.OrderByPrimaryKeyIfUnordered(context); + + Assert.That(result, Is.SameAs(ordered)); + } + private sealed class ConstantValueFinder(object target) : ExpressionVisitor { public bool Found { get; private set; } From fa796434762d7eab3bdbea4514025c40b48c0b91 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 10:23:30 +0000 Subject: [PATCH 15/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20mark=20Phase=201?= =?UTF-8?q?=20complete=20(C2,=20P2,=20A5);=20add=20MongoDb.Tests=20to=20so?= =?UTF-8?q?lution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- docs/IMPROVEMENT_PLAN.md | 31 ++-- eQuantic.Core.Data.EntityFramework.sln | 223 ++++++++++++++++++++++++- 2 files changed, 242 insertions(+), 12 deletions(-) diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md index 72c87dd..c307d31 100644 --- a/docs/IMPROVEMENT_PLAN.md +++ b/docs/IMPROVEMENT_PLAN.md @@ -31,9 +31,9 @@ O plano abaixo está em **5 fases**. As Fases 0–2 **não quebram contrato** e As Fases 3–4 definem a **v5.0.0 dos contratos** (breaking deliberado) e a estratégia de versionamento no nuget.org. -## Status de implementação (Fase 1 — grande parte concluída) +## Status de implementação (Fase 1 — concluída) -Correções já aplicadas nesta branch, **com testes** (21 testes passando; todos os 5 pacotes compilando — +Correções já aplicadas nesta branch, **com testes** (28 testes passando; todos os 5 pacotes compilando — base/SqlServer/PostgreSql/MySql em net10, MongoDb em net8): | Achado | Correção | Commit | @@ -47,22 +47,33 @@ base/SqlServer/PostgreSql/MySql em net10, MongoDb em net8): | **A4/M7** chave | Valor da chave parametrizado (closure) em vez de literal; metadados de PK cacheados; `EF.Property` para shadow keys; lança em chave composta parcial. | `⚡ fix(core)` | | **M4** DI | `AddRepository` respeita o lifetime configurado, usa `TryAdd`, e tolera `ReflectionTypeLoadException`. | `🐞 fix(di)` | | **M2** async | `ConfigureAwait(false)` em 91 awaits de biblioteca (base + 4 providers). | `⚡ perf(async)` | +| **P2** MongoDb | `UpdateDefinitionBuilder` rejeita (lança) updates que referenciam a entidade em vez de gravar constante silenciosamente. Novo projeto de testes do MongoDb. | `🐞 fix(mongodb)` | +| **C2** ownership ⚠️ | O repositório **não** dispõe mais o `UnitOfWork` injetado (o criador — container DI ou chamador — é dono do ciclo de vida). **Mudança comportamental.** | `🐞 fix(core)` | +| **A5** paginação | `GetPaged`/`GetPagedAsync` ordenam pela PK quando não há ordenação explícita (paginação determinística); ordenação do chamador é preservada. | `🐞 fix(read)` | Substituído o teste placebo (`Assert.Pass()`) por cobertura real: DI, disposal, parametrização de SQL, -queries e expressão de chave via EF InMemory. +queries, expressão de chave, paginação e `UpdateDefinitionBuilder` do MongoDb, via EF InMemory. + +⚠️ **C2 é a única mudança comportamental do lote.** Quem dependia de dispor o repositório para fechar um +contexto criado manualmente passa a precisar dispor o `UnitOfWork`/`DbContext` diretamente (o container +DI já faz isso). Documentado no commit. **Investigado e corrigido no diagnóstico:** o item **M8** do plano (remover `Where(_ => true)` em `GetAllAsync`) estava **errado** — esse `Where` é load-bearing (o `SetBase` não implementa `IAsyncEnumerable`; o `Where` o converte num `IQueryable` real do EF). Documentado no código + teste de regressão; nada removido. -**Pendente na Fase 1 (precisa de decisão/design, deixado de fora deste lote):** -- **C2** — ownership do `UnitOfWork` injetado: hoje o repositório dispõe o UoW recebido por DI. Corrigir - é a coisa certa, mas muda comportamento observável — decisão de produto. -- **P2** — MongoDb `UpdateDefinitionBuilder` grava constante (`x => x.Count + 1` vira `1`): a correção é - **rejeitar** (lançar) ou **traduzir para `$inc`** — esforços bem diferentes. -- **A5** — paginação sem `OrderBy`: um fallback seguro por PK precisa do model na camada do `Set` (4 - providers), não é one-liner no repo base. +**Fase 1 concluída.** Próximos passos (fases maiores, arquiteturais/breaking — aguardam definição de +abordagem): +- **Fase 0** — separar CI de release, gatear a publicação por tag/environment protegido, adotar MinVer, + remover o `build/` (MSBump morto). +- **Fase 2** — de-duplicação dos providers (~2.400 linhas idênticas → base com hooks de dialeto). +- **Fase 3/4** — v5.0.0 dos contratos (`eQuantic.Core.Data`) e consolidação das linhas de versão no + nuget.org (ver Partes III e IV). + +Nota sobre o **P2**: a correção atual **rejeita** updates que referenciam a entidade (evita corrupção +silenciosa). Suportá-los de fato via `$inc`/pipeline updates fica para uma iteração futura do provider +MongoDb. --- diff --git a/eQuantic.Core.Data.EntityFramework.sln b/eQuantic.Core.Data.EntityFramework.sln index 7630e09..140de93 100644 --- a/eQuantic.Core.Data.EntityFramework.sln +++ b/eQuantic.Core.Data.EntityFramework.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26430.6 @@ -74,112 +74,330 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Tests", "tests\eQuantic.Core.Data.EntityFramework.SqlServer.Tests\eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj", "{A730FE2E-2972-41F6-AAF7-3AEE017A7418}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MongoDb.Tests", "tests\eQuantic.Core.Data.EntityFramework.MongoDb.Tests\eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj", "{8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|x64.ActiveCfg = Debug|Any CPU + {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|x64.Build.0 = Debug|Any CPU + {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|x86.ActiveCfg = Debug|Any CPU + {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|x86.Build.0 = Debug|Any CPU {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|Any CPU.Build.0 = Release|Any CPU + {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|x64.ActiveCfg = Release|Any CPU + {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|x64.Build.0 = Release|Any CPU + {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|x86.ActiveCfg = Release|Any CPU + {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|x86.Build.0 = Release|Any CPU {20175F0B-5566-4213-A60E-435A9458B018}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {20175F0B-5566-4213-A60E-435A9458B018}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20175F0B-5566-4213-A60E-435A9458B018}.Debug|x64.ActiveCfg = Debug|Any CPU + {20175F0B-5566-4213-A60E-435A9458B018}.Debug|x64.Build.0 = Debug|Any CPU + {20175F0B-5566-4213-A60E-435A9458B018}.Debug|x86.ActiveCfg = Debug|Any CPU + {20175F0B-5566-4213-A60E-435A9458B018}.Debug|x86.Build.0 = Debug|Any CPU {20175F0B-5566-4213-A60E-435A9458B018}.Release|Any CPU.ActiveCfg = Release|Any CPU {20175F0B-5566-4213-A60E-435A9458B018}.Release|Any CPU.Build.0 = Release|Any CPU + {20175F0B-5566-4213-A60E-435A9458B018}.Release|x64.ActiveCfg = Release|Any CPU + {20175F0B-5566-4213-A60E-435A9458B018}.Release|x64.Build.0 = Release|Any CPU + {20175F0B-5566-4213-A60E-435A9458B018}.Release|x86.ActiveCfg = Release|Any CPU + {20175F0B-5566-4213-A60E-435A9458B018}.Release|x86.Build.0 = Release|Any CPU {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|x64.ActiveCfg = Debug|Any CPU + {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|x64.Build.0 = Debug|Any CPU + {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|x86.ActiveCfg = Debug|Any CPU + {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|x86.Build.0 = Debug|Any CPU {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|Any CPU.ActiveCfg = Release|Any CPU {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|Any CPU.Build.0 = Release|Any CPU + {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|x64.ActiveCfg = Release|Any CPU + {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|x64.Build.0 = Release|Any CPU + {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|x86.ActiveCfg = Release|Any CPU + {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|x86.Build.0 = Release|Any CPU {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|x64.Build.0 = Debug|Any CPU + {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|x86.Build.0 = Debug|Any CPU {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|Any CPU.Build.0 = Release|Any CPU + {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|x64.ActiveCfg = Release|Any CPU + {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|x64.Build.0 = Release|Any CPU + {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|x86.ActiveCfg = Release|Any CPU + {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|x86.Build.0 = Release|Any CPU {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|x64.ActiveCfg = Debug|Any CPU + {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|x64.Build.0 = Debug|Any CPU + {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|x86.ActiveCfg = Debug|Any CPU + {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|x86.Build.0 = Debug|Any CPU {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|Any CPU.ActiveCfg = Release|Any CPU {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|Any CPU.Build.0 = Release|Any CPU + {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|x64.ActiveCfg = Release|Any CPU + {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|x64.Build.0 = Release|Any CPU + {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|x86.ActiveCfg = Release|Any CPU + {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|x86.Build.0 = Release|Any CPU {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|x64.Build.0 = Debug|Any CPU + {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|x86.Build.0 = Debug|Any CPU {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|Any CPU.ActiveCfg = Release|Any CPU {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|Any CPU.Build.0 = Release|Any CPU + {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|x64.ActiveCfg = Release|Any CPU + {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|x64.Build.0 = Release|Any CPU + {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|x86.ActiveCfg = Release|Any CPU + {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|x86.Build.0 = Release|Any CPU {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|x64.Build.0 = Debug|Any CPU + {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|x86.Build.0 = Debug|Any CPU {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|Any CPU.Build.0 = Release|Any CPU + {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|x64.ActiveCfg = Release|Any CPU + {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|x64.Build.0 = Release|Any CPU + {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|x86.ActiveCfg = Release|Any CPU + {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|x86.Build.0 = Release|Any CPU {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|x64.ActiveCfg = Debug|Any CPU + {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|x64.Build.0 = Debug|Any CPU + {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|x86.ActiveCfg = Debug|Any CPU + {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|x86.Build.0 = Debug|Any CPU {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|Any CPU.ActiveCfg = Release|Any CPU {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|Any CPU.Build.0 = Release|Any CPU + {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|x64.ActiveCfg = Release|Any CPU + {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|x64.Build.0 = Release|Any CPU + {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|x86.ActiveCfg = Release|Any CPU + {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|x86.Build.0 = Release|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|x64.Build.0 = Debug|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|x86.Build.0 = Debug|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|Any CPU.Build.0 = Release|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x64.ActiveCfg = Release|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x64.Build.0 = Release|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x86.ActiveCfg = Release|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x86.Build.0 = Release|Any CPU {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|x64.ActiveCfg = Debug|Any CPU + {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|x64.Build.0 = Debug|Any CPU + {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|x86.ActiveCfg = Debug|Any CPU + {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|x86.Build.0 = Debug|Any CPU {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|Any CPU.ActiveCfg = Release|Any CPU {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|Any CPU.Build.0 = Release|Any CPU + {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|x64.ActiveCfg = Release|Any CPU + {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|x64.Build.0 = Release|Any CPU + {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|x86.ActiveCfg = Release|Any CPU + {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|x86.Build.0 = Release|Any CPU {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|x64.ActiveCfg = Debug|Any CPU + {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|x64.Build.0 = Debug|Any CPU + {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|x86.ActiveCfg = Debug|Any CPU + {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|x86.Build.0 = Debug|Any CPU {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|Any CPU.ActiveCfg = Release|Any CPU {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|Any CPU.Build.0 = Release|Any CPU + {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|x64.ActiveCfg = Release|Any CPU + {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|x64.Build.0 = Release|Any CPU + {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|x86.ActiveCfg = Release|Any CPU + {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|x86.Build.0 = Release|Any CPU {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|x64.ActiveCfg = Debug|Any CPU + {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|x64.Build.0 = Debug|Any CPU + {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|x86.ActiveCfg = Debug|Any CPU + {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|x86.Build.0 = Debug|Any CPU {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|Any CPU.Build.0 = Release|Any CPU + {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|x64.ActiveCfg = Release|Any CPU + {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|x64.Build.0 = Release|Any CPU + {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|x86.ActiveCfg = Release|Any CPU + {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|x86.Build.0 = Release|Any CPU {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|x64.ActiveCfg = Debug|Any CPU + {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|x64.Build.0 = Debug|Any CPU + {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|x86.ActiveCfg = Debug|Any CPU + {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|x86.Build.0 = Debug|Any CPU {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|Any CPU.ActiveCfg = Release|Any CPU {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|Any CPU.Build.0 = Release|Any CPU + {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|x64.ActiveCfg = Release|Any CPU + {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|x64.Build.0 = Release|Any CPU + {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|x86.ActiveCfg = Release|Any CPU + {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|x86.Build.0 = Release|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|x64.ActiveCfg = Debug|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|x64.Build.0 = Debug|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|x86.ActiveCfg = Debug|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|x86.Build.0 = Debug|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|Any CPU.Build.0 = Release|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x64.ActiveCfg = Release|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x64.Build.0 = Release|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x86.ActiveCfg = Release|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x86.Build.0 = Release|Any CPU {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|x64.Build.0 = Debug|Any CPU + {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|x86.Build.0 = Debug|Any CPU {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|Any CPU.Build.0 = Release|Any CPU + {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|x64.ActiveCfg = Release|Any CPU + {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|x64.Build.0 = Release|Any CPU + {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|x86.ActiveCfg = Release|Any CPU + {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|x86.Build.0 = Release|Any CPU {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|x64.Build.0 = Debug|Any CPU + {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|x86.Build.0 = Debug|Any CPU {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|Any CPU.Build.0 = Release|Any CPU + {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|x64.ActiveCfg = Release|Any CPU + {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|x64.Build.0 = Release|Any CPU + {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|x86.ActiveCfg = Release|Any CPU + {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|x86.Build.0 = Release|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|x64.ActiveCfg = Debug|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|x64.Build.0 = Debug|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|x86.ActiveCfg = Debug|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|x86.Build.0 = Debug|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|Any CPU.ActiveCfg = Release|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|Any CPU.Build.0 = Release|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x64.ActiveCfg = Release|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x64.Build.0 = Release|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x86.ActiveCfg = Release|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x86.Build.0 = Release|Any CPU {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|x64.Build.0 = Debug|Any CPU + {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|x86.Build.0 = Debug|Any CPU {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|Any CPU.Build.0 = Release|Any CPU + {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|x64.ActiveCfg = Release|Any CPU + {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|x64.Build.0 = Release|Any CPU + {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|x86.ActiveCfg = Release|Any CPU + {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|x86.Build.0 = Release|Any CPU {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|x64.ActiveCfg = Debug|Any CPU + {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|x64.Build.0 = Debug|Any CPU + {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|x86.ActiveCfg = Debug|Any CPU + {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|x86.Build.0 = Debug|Any CPU {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|Any CPU.Build.0 = Release|Any CPU + {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|x64.ActiveCfg = Release|Any CPU + {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|x64.Build.0 = Release|Any CPU + {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|x86.ActiveCfg = Release|Any CPU + {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|x86.Build.0 = Release|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|x64.ActiveCfg = Debug|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|x64.Build.0 = Debug|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|x86.ActiveCfg = Debug|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|x86.Build.0 = Debug|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|Any CPU.ActiveCfg = Release|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|Any CPU.Build.0 = Release|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|x64.ActiveCfg = Release|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|x64.Build.0 = Release|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|x86.ActiveCfg = Release|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|x86.Build.0 = Release|Any CPU {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|x64.ActiveCfg = Debug|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|x64.Build.0 = Debug|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|x86.ActiveCfg = Debug|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|x86.Build.0 = Debug|Any CPU {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|Any CPU.Build.0 = Release|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|x64.ActiveCfg = Release|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|x64.Build.0 = Release|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|x86.ActiveCfg = Release|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|x86.Build.0 = Release|Any CPU {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|x64.ActiveCfg = Debug|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|x64.Build.0 = Debug|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|x86.Build.0 = Debug|Any CPU {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|Any CPU.Build.0 = Release|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|x64.ActiveCfg = Release|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|x64.Build.0 = Release|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|x86.ActiveCfg = Release|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|x86.Build.0 = Release|Any CPU {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|x64.Build.0 = Debug|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|x86.Build.0 = Debug|Any CPU {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|Any CPU.Build.0 = Release|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|x64.ActiveCfg = Release|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|x64.Build.0 = Release|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|x86.ActiveCfg = Release|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|x86.Build.0 = Release|Any CPU {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|x64.Build.0 = Debug|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|x86.Build.0 = Debug|Any CPU {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|Any CPU.Build.0 = Release|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|x64.ActiveCfg = Release|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|x64.Build.0 = Release|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|x86.ActiveCfg = Release|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|x86.Build.0 = Release|Any CPU {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|x64.ActiveCfg = Debug|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|x64.Build.0 = Debug|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|x86.ActiveCfg = Debug|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|x86.Build.0 = Debug|Any CPU {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|Any CPU.ActiveCfg = Release|Any CPU {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|Any CPU.Build.0 = Release|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|x64.ActiveCfg = Release|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|x64.Build.0 = Release|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|x86.ActiveCfg = Release|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|x86.Build.0 = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|x64.ActiveCfg = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|x64.Build.0 = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|x86.ActiveCfg = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|x86.Build.0 = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|Any CPU.Build.0 = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x64.ActiveCfg = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x64.Build.0 = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x86.ActiveCfg = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -189,8 +407,8 @@ Global {20175F0B-5566-4213-A60E-435A9458B018} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {AB04C373-0DBD-4B6F-811D-D668851BBAD5} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {91233051-EE7E-4CBB-8FFD-B900ABBACBBE} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} + {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {F613B6F2-C420-4E72-981A-47A20A538BC6} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D} = {F613B6F2-C420-4E72-981A-47A20A538BC6} {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} @@ -214,6 +432,7 @@ Global {B5E862DD-A02A-4E26-AA83-687C608C4203} = {F613B6F2-C420-4E72-981A-47A20A538BC6} {D0A7C1F3-94B3-4353-9C14-827ABBAD649D} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} {A730FE2E-2972-41F6-AAF7-3AEE017A7418} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83FECDD1-8A97-40B1-8529-3D5216E674C3} From 2757465bdee714ad0a95288319d4daf234a50b44 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 20:34:54 +0000 Subject: [PATCH 16/32] =?UTF-8?q?=F0=9F=94=A5=20chore:=20remove=20dead=20M?= =?UTF-8?q?SBump/Directory.Build.targets;=20fix=20PK2=20project=20ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MSBump.props imported itself (circular — MSBuild would ignore it), and MSBump.targets called the BumpVersion task without any UsingTask or package reference anywhere in the repo. build/Directory.Build.targets lives outside any project's ancestor directory chain, so MSBuild never auto-imported it either — none of these three files were reachable from any csproj (confirmed by grep). Even if they had been wired up, MSBump bumps the version on every local build, which produces non-deterministic, non-reproducible package versions unrelated to the actual commit. Also fix a real bug found while auditing the *.NetX.csproj variants: MySql.Net10.csproj referenced the base package's Net9 project instead of Net10, so the MySql 10.0.x package would declare a dependency on the net9.0-only line of eQuantic.Core.Data.EntityFramework. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- .github/workflows/ci.yml | 80 ++++++++++++ .github/workflows/dotnetcore.yml | 67 ---------- .github/workflows/release.yml | 114 ++++++++++++++++++ build/Directory.Build.targets | 10 -- build/MSBump.props | 6 - build/MSBump.targets | 41 ------- global.json | 6 + ...re.Data.EntityFramework.MySql.Net10.csproj | 2 +- .../.msbump | 13 -- 9 files changed, 201 insertions(+), 138 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/dotnetcore.yml create mode 100644 .github/workflows/release.yml delete mode 100644 build/Directory.Build.targets delete mode 100644 build/MSBump.props delete mode 100644 build/MSBump.targets create mode 100644 global.json delete mode 100644 src/eQuantic.Core.Data.EntityFramework/.msbump diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6f2c159 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + pull_request: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build ${{ matrix.project }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + project: + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Build + run: dotnet build ${{ matrix.project }} --configuration Release -p:ContinuousIntegrationBuild=true + + test: + name: Test ${{ matrix.project }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + project: + - tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj + - tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj + - tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Test + run: dotnet test ${{ matrix.project }} --configuration Release diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml deleted file mode 100644 index 372c6ff..0000000 --- a/.github/workflows/dotnetcore.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: eQuantic Core Data EntityFramework - -on: [push] - -jobs: - build: - runs-on: windows-latest - - steps: - - uses: actions/checkout@v3 - - name: Setup .NET Core - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 10.0.x - - name: Build eQuantic.Core.Data.EntityFramework Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 6 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 7 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 8 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj --configuration Release - - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 6 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 7 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 8 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj --configuration Release - - - name: Build eQuantic.Core.Data.EntityFramework.MySql Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MySql .net 8 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MySql .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MySql .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj --configuration Release - - - name: Build eQuantic.Core.Data.EntityFramework.PostgreSql Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.PostgreSql .net 8 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.PostgreSql .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.PostgreSql .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj --configuration Release - - - name: Build eQuantic.Core.Data.EntityFramework.MongoDb Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MongoDb .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MongoDb .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj --configuration Release - - - name: Push package into Nuget.org - run: dotnet nuget push **/*.nupkg --skip-duplicate -k ${{secrets.nuget_key}} -s https://api.nuget.org/v3/index.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..920694e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,114 @@ +name: Release + +# Only a pushed version tag (e.g. v10.0.3) triggers a release. A plain push to any branch — including +# master — no longer publishes anything; that was the previous, unintentional behavior (see +# docs/IMPROVEMENT_PLAN.md, finding Q1). To release: `git tag vX.Y.Z && git push origin vX.Y.Z`. +on: + push: + tags: + - 'v*.*.*' + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: Build ${{ matrix.project }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + project: + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Build & pack + run: dotnet build ${{ matrix.project }} --configuration Release -p:ContinuousIntegrationBuild=true + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: nupkg-${{ strategy.job-index }} + path: artifacts/*.nupkg + if-no-files-found: error + retention-days: 7 + + test: + name: Test ${{ matrix.project }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + project: + - tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj + - tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj + - tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Test + run: dotnet test ${{ matrix.project }} --configuration Release + + publish: + name: Publish to NuGet.org + runs-on: ubuntu-latest + needs: [build, test] + # This environment gates the publish behind whatever protection rules are configured for it in + # the repo's Settings -> Environments (e.g. required reviewers). GitHub auto-creates an environment + # on first use with NO protection rules, so a repo admin must add them for the gate to be effective. + environment: + name: nuget-release + url: https://www.nuget.org/profiles/eQuantic + steps: + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Download all package artifacts + uses: actions/download-artifact@v4 + with: + pattern: nupkg-* + path: artifacts + merge-multiple: true + - name: Push to NuGet.org + run: dotnet nuget push "artifacts/*.nupkg" --skip-duplicate -k ${{ secrets.nuget_key }} -s https://api.nuget.org/v3/index.json diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets deleted file mode 100644 index 486eff8..0000000 --- a/build/Directory.Build.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/build/MSBump.props b/build/MSBump.props deleted file mode 100644 index 21486c9..0000000 --- a/build/MSBump.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/build/MSBump.targets b/build/MSBump.targets deleted file mode 100644 index 66d54cc..0000000 --- a/build/MSBump.targets +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - $(MSBumpNewVersion) - $(MSBumpNewVersion) - - - - - - - - - True - - - \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..512142d --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj index d283474..7277f70 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj @@ -64,6 +64,6 @@ + Include="..\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net10.csproj" /> \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/.msbump b/src/eQuantic.Core.Data.EntityFramework/.msbump deleted file mode 100644 index 6850641..0000000 --- a/src/eQuantic.Core.Data.EntityFramework/.msbump +++ /dev/null @@ -1,13 +0,0 @@ -{ - Configurations: { - "Debug": { - BumpLabel: "dev", - LabelDigits: 4 - }, - - "Release": { - BumpRevision: true, - ResetLabel: "dev" - } - } -} \ No newline at end of file From 15a229829aff6862b5797ef39b95a1ebb7cd6c75 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 20:37:17 +0000 Subject: [PATCH 17/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20record=20Phase=200?= =?UTF-8?q?=20status=20(CI/release=20split,=20dead=20build/=20removal)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s --- docs/IMPROVEMENT_PLAN.md | 50 ++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md index c307d31..028590e 100644 --- a/docs/IMPROVEMENT_PLAN.md +++ b/docs/IMPROVEMENT_PLAN.md @@ -63,18 +63,54 @@ DI já faz isso). Documentado no commit. `IAsyncEnumerable`; o `Where` o converte num `IQueryable` real do EF). Documentado no código + teste de regressão; nada removido. -**Fase 1 concluída.** Próximos passos (fases maiores, arquiteturais/breaking — aguardam definição de -abordagem): -- **Fase 0** — separar CI de release, gatear a publicação por tag/environment protegido, adotar MinVer, - remover o `build/` (MSBump morto). -- **Fase 2** — de-duplicação dos providers (~2.400 linhas idênticas → base com hooks de dialeto). -- **Fase 3/4** — v5.0.0 dos contratos (`eQuantic.Core.Data`) e consolidação das linhas de versão no - nuget.org (ver Partes III e IV). +**Fase 1 concluída.** Nota sobre o **P2**: a correção atual **rejeita** updates que referenciam a entidade (evita corrupção silenciosa). Suportá-los de fato via `$inc`/pipeline updates fica para uma iteração futura do provider MongoDb. +## Status de implementação (Fase 0 — concluída, escopo reduzido) + +| Achado | Correção | +|--------|----------| +| **PK7** MSBump morto | Removido `build/MSBump.props` (import circular), `build/MSBump.targets` (task inexistente) e `build/Directory.Build.targets` (fora da cadeia de ancestrais — nunca era importado). Nada os referenciava; confirmado por grep antes de apagar. | +| **PK2** bug de grafo | `MySql.Net10.csproj` referenciava o core **Net9** em vez do **Net10** — corrigido. | +| **Q1** publicação por acidente | CI dividido em **`ci.yml`** (build + test em todo push/PR, sem publicar nada) e **`release.yml`** (só dispara em tag `vX.Y.Z`, publica atrás de um GitHub Environment `nuget-release`). | +| **Q2** sem testes no CI | `ci.yml` roda `dotnet test` nos 3 projetos de teste (antes não rodava nenhum). | +| **Q3** pipeline datado | Actions atualizadas (`checkout@v4`, `setup-dotnet@v4`), `ubuntu-latest` no lugar de `windows-latest`, cache de NuGet, `-p:ContinuousIntegrationBuild=true`. | +| **Q4** SDK não fixado | `global.json` na raiz fixando `10.0.100` com `rollForward: latestFeature`. | + +**Decisão de design (build por matriz, não por `dotnet build sln`):** tentei rodar +`dotnet build eQuantic.Core.Data.EntityFramework.sln` localmente para simplificar os 23 steps de build — +e reproduzi exatamente o problema do achado **PK4**: como vários `.csproj` de um mesmo pacote +(`eQuantic.Core.Data.EntityFramework.csproj`, `.Net6.csproj`, `.Net7.csproj`, …) compartilham a mesma +pasta sem `BaseIntermediateOutputPath` próprio, o build paralelo da solution corrompeu o +`project.assets.json` de uns com os outros e um `IOException` de arquivo em uso em outro. Os dois +workflows novos mantêm os 23 builds **individuais** (como o workflow antigo já fazia), mas cada um roda +numa **matrix job** — ou seja, em runner/checkout isolado — o que elimina o compartilhamento de +`obj`/`bin` sem precisar resolver a decisão de versionamento (Parte IV) primeiro. + +⚠️ **Duas coisas que só um mantenedor com acesso ao GitHub consegue terminar:** +1. **Nada publica automaticamente até existir uma tag.** Antes, qualquer push em qualquer branch tentava + publicar (mitigado só por `--skip-duplicate`). Agora é preciso `git tag vX.Y.Z && git push origin + vX.Y.Z` para disparar o `release.yml`. Isso é intencional (achado Q1), mas muda o fluxo de trabalho. +2. **O `environment: nuget-release` referenciado no `release.yml` não tem proteção nenhuma até ser + configurado.** O GitHub cria o Environment automaticamente no primeiro uso, sem revisores obrigatórios + nem restrição de branch/tag — a ferramenta de PR desta sessão não tem permissão para configurar isso. + Em *Settings → Environments → nuget-release*, adicionar ao menos um revisor obrigatório para o gate + funcionar de verdade. + +**Deliberadamente fora do escopo desta Fase 0** (não fiz, porque dependem da decisão de versionamento +ainda em aberto — Parte IV): adoção de MinVer (mexeria em como a `` de cada um dos 23 csproj é +determinada) e qualquer mudança nos números de versão publicados. Fazer isso agora, antes de decidir entre +consolidar numa linha única (opção A) ou manter `PackageId`s separados por .NET (opção B), arriscaria +retrabalho. + +**Próximos passos** (fases maiores, arquiteturais/breaking — aguardam definição de abordagem): +- **Fase 2** — de-duplicação dos providers (~2.400 linhas idênticas → base com hooks de dialeto). +- **Fase 3/4** — v5.0.0 dos contratos (`eQuantic.Core.Data`) e consolidação das linhas de versão no + nuget.org (ver Partes III e IV) — inclui a decisão de versionamento que bloqueia o MinVer. + --- ## Parte I — Diagnóstico From c59fb6e3ccf519d16b19a296c8bc7c72d9723c28 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 15:44:58 +0000 Subject: [PATCH 18/32] refactor: extract shared relational implementation into a single package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SqlServer, PostgreSql and MySql providers were near-identical copies: SqlExecutor and UnitOfWork were byte-for-byte the same, Set differed only in the net6 Z.EntityFramework.Plus branches, and ExpressionConverter was duplicated verbatim. That copy-paste is what let the EXEC-vs-CALL dialect bug diverge between providers. Introduce eQuantic.Core.Data.EntityFramework.Relational holding the shared implementation once (RelationalSqlExecutor, RelationalUnitOfWork, RelationalSet, and the internal ExpressionConverter / SqlConfigurationExtensions). The only dialect difference — stored procedures use EXEC on SQL Server and the ANSI CALL elsewhere — is a single BuildProcedureSql virtual, overridden only by SQL Server. Each provider keeps thin Set and UnitOfWork subclasses plus DefaultUnitOfWork, so the consumer-facing types stay in their namespaces. Net effect: ~2,200 fewer lines of duplicated source. All provider csprojs (multi-target and per-framework) reference the shared project; MySql's per-framework variants are realigned to the multi-target base project (matching SqlServer/PostgreSql) so the shared project does not pull a second copy of the base assembly. Note: the implementation-only public types SqlExecutor, the non-generic UnitOfWork and SqlConfigurationExtensions move to the Relational namespace; GetEntityByIdSpecification now takes RelationalUnitOfWork. All 28 tests pass and every package builds across net6-net10 (MongoDb net8). --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 1 + eQuantic.Core.Data.EntityFramework.sln | 15 + .../Repository/Set.cs | 264 +-------- .../Repository/SqlExecutor.cs | 529 ------------------ .../Repository/UnitOfWork.cs | 266 +-------- ...re.Data.EntityFramework.MySql.Net10.csproj | 4 +- ...ore.Data.EntityFramework.MySql.Net8.csproj | 4 +- ...ore.Data.EntityFramework.MySql.Net9.csproj | 4 +- ...tic.Core.Data.EntityFramework.MySql.csproj | 2 + .../ExpressionConverter.cs | 176 ------ .../Repository/Set.cs | 264 +-------- .../Repository/UnitOfWork.cs | 266 +-------- ...ta.EntityFramework.PostgreSql.Net10.csproj | 2 + ...ata.EntityFramework.PostgreSql.Net8.csproj | 2 + ...ata.EntityFramework.PostgreSql.Net9.csproj | 2 + ...ore.Data.EntityFramework.PostgreSql.csproj | 2 + .../ExpressionConverter.cs | 34 +- .../Extensions/SqlConfigurationExtensions.cs | 14 + .../Repository/RelationalSet.cs | 290 ++++++++++ .../Repository/RelationalSqlExecutor.cs} | 75 +-- .../Repository/RelationalUnitOfWork.cs | 270 +++++++++ ...ore.Data.EntityFramework.Relational.csproj | 83 +++ .../ExpressionConverter.cs | 176 ------ .../Repository/Set.cs | 283 +--------- .../Repository/SqlExecutor.cs | 528 ----------------- .../Repository/UnitOfWork.cs | 265 +-------- .../GetEntityByIdSpecification.cs | 8 +- ...ata.EntityFramework.SqlServer.Net10.csproj | 2 + ...Data.EntityFramework.SqlServer.Net6.csproj | 2 + ...Data.EntityFramework.SqlServer.Net7.csproj | 2 + ...Data.EntityFramework.SqlServer.Net8.csproj | 2 + ...Data.EntityFramework.SqlServer.Net9.csproj | 2 + ...Core.Data.EntityFramework.SqlServer.csproj | 2 + .../eQuantic.Core.Data.EntityFramework.csproj | 3 + .../ExpressionConverterTests.cs | 1 + .../SqlExecutorParameterizationTests.cs | 39 +- 37 files changed, 843 insertions(+), 3042 deletions(-) delete mode 100644 src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs delete mode 100644 src/eQuantic.Core.Data.EntityFramework.PostgreSql/ExpressionConverter.cs rename src/{eQuantic.Core.Data.EntityFramework.MySql => eQuantic.Core.Data.EntityFramework.Relational}/ExpressionConverter.cs (93%) create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs rename src/{eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs => eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs} (92%) create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework.SqlServer/ExpressionConverter.cs delete mode 100644 src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f2c159..6445338 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 920694e..609fd72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,7 @@ jobs: - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj diff --git a/eQuantic.Core.Data.EntityFramework.sln b/eQuantic.Core.Data.EntityFramework.sln index 140de93..4895d72 100644 --- a/eQuantic.Core.Data.EntityFramework.sln +++ b/eQuantic.Core.Data.EntityFramework.sln @@ -76,6 +76,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MongoDb.Tests", "tests\eQuantic.Core.Data.EntityFramework.MongoDb.Tests\eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj", "{8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Relational", "src\eQuantic.Core.Data.EntityFramework.Relational\eQuantic.Core.Data.EntityFramework.Relational.csproj", "{A6953967-90B5-485D-956E-A62EFE67423F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -398,6 +400,18 @@ Global {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x64.Build.0 = Release|Any CPU {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x86.ActiveCfg = Release|Any CPU {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x86.Build.0 = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|x64.Build.0 = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|x86.Build.0 = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|Any CPU.Build.0 = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x64.ActiveCfg = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x64.Build.0 = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x86.ActiveCfg = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -433,6 +447,7 @@ Global {D0A7C1F3-94B3-4353-9C14-827ABBAD649D} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} {A730FE2E-2972-41F6-AAF7-3AEE017A7418} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} + {A6953967-90B5-485D-956E-A62EFE67423F} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83FECDD1-8A97-40B1-8529-3D5216E674C3} diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs index 4cef9e3..6194111 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs @@ -1,268 +1,16 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; namespace eQuantic.Core.Data.EntityFramework.MySql.Repository; -public class Set : SetBase where TEntity : class, IEntity, new() +/// +/// MySQL entity set. The implementation lives in ; this type +/// is preserved for source compatibility. +/// +public class Set : RelationalSet where TEntity : class, IEntity, new() { public Set(DbContext context) : base(context) { } - - public override long DeleteMany(Expression> filter) - { - return InternalDbSet.Where(filter).ExecuteDelete(); - } - - public override async Task DeleteManyAsync(Expression> filter, - CancellationToken cancellationToken = default) - { - return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - } - - public void LoadCollection(TChildEntity item, - Expression>> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Collection(selector).Load(); - } - - public void LoadCollection(TChildEntity item, string propertyName) - where TChildEntity : class - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - - public async Task LoadCollectionAsync(TChildEntity item, - Expression>> selector) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Collection(selector).LoadAsync().ConfigureAwait(false); - } - - public async Task LoadCollectionAsync(TChildEntity item, string propertyName) - where TChildEntity : class - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync().ConfigureAwait(false); - } - - public void LoadProperties(TEntity entity, params string[] properties) - { - if (properties is not { Length: > 0 }) - { - return; - } - - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - LoadProperty(entity, property); - } - else - { - LoadCascade(props, entity); - } - } - } - - public async Task LoadPropertiesAsync(TEntity entity, params string[] properties) - { - if (properties is { Length: > 0 }) - { - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - await LoadPropertyAsync(entity, property).ConfigureAwait(false); - } - else - { - await LoadCascadeAsync(props, entity).ConfigureAwait(false); - } - } - } - } - - public void LoadProperty(TChildEntity item, - Expression> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Reference(selector).Load(); - } - - public void LoadProperty(TChildEntity item, string propertyName) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - else - { - DbContext.Entry(item).Reference(propertyName).Load(); - } - } - - public async Task LoadPropertyAsync(TChildEntity item, - Expression> selector, CancellationToken cancellationToken = default) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task LoadPropertyAsync(TChildEntity item, string propertyName, - CancellationToken cancellationToken = default) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); - } - else - { - await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); - } - } - - public override long UpdateMany(Expression> filter, - Expression> updateExpression) - { - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return InternalDbSet.Where(filter).ExecuteUpdate(convertedExpression); - } - - public override async Task UpdateManyAsync(Expression> filter, - Expression> updateExpression, CancellationToken cancellationToken = default) - { - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken).ConfigureAwait(false); - } - - private void LoadCascade(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - LoadProperty(obj, props[index]); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - LoadCascade(props, nextObj, index + 1); - } - } - - private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - await LoadPropertyAsync(obj, props[index]).ConfigureAwait(false); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - await LoadCascadeAsync(props, nextObj, index + 1).ConfigureAwait(false); - } - } - - internal Expression> GetExpression(TKey id) - { - return DbContext.GetFindByKeyExpression(id); - } - - public override IQueryable GetQueryable(Action configuration, - Func, IQueryable> internalQueryAction) - { - if (configuration == null) - { - return internalQueryAction.Invoke(this); - } - - var config = GetConfig(configuration); - var queryableConfig = config as QueryableConfiguration; - - var query = string.IsNullOrEmpty(queryableConfig?.SqlRaw) ? this : InternalDbSet.FromSqlRaw(queryableConfig.SqlRaw); - - if (config.HasNoTracking) - { - query = query.AsNoTracking(); - } - - if (config.Properties?.Any() == true) - { - query = query.IncludeMany(config.Properties.ToArray()); - } - - if (queryableConfig?.IgnoreQueryFilters == true) - { - query = query.IgnoreQueryFilters(); - } - - if (!string.IsNullOrEmpty(config.Tag)) - { - query = query.TagWith(config.Tag); - } - - if (queryableConfig != null) - { - query = queryableConfig.BeforeCustomization.Invoke(query); - } - - query = internalQueryAction.Invoke(query); - - if (config.SortingColumns.Any()) - { - query = query.OrderBy(config.SortingColumns.ToArray()); - } - - if (queryableConfig != null) - { - query = queryableConfig.AfterCustomization.Invoke(query); - } - - return query; - } } diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs deleted file mode 100644 index 478a9f3..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs +++ /dev/null @@ -1,529 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.MySql.Repository.Extensions; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Sql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; - -namespace eQuantic.Core.Data.EntityFramework.MySql.Repository; - -/// -/// The sql executor class -/// -/// -/// -/// -[ExcludeFromCodeCoverage] -public abstract class SqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable -{ - /// - /// The context - /// - protected readonly DbContext Context; - - /// - /// The disposed - /// - protected bool Disposed; - - /// - /// The transaction - /// - protected IDbContextTransaction Transaction; - - /// - /// Initializes a new instance of the class - /// - /// The context - protected SqlExecutor(DbContext context) - { - Context = context; - } - - /// - /// Begins the transaction using the specified cancellation token - /// - /// The cancellation token - public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) - { - Transaction?.Dispose(); - Transaction = await Context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - } - - /// - /// Commits the transaction using the specified cancellation token - /// - /// The cancellation token - public virtual async Task CommitTransactionAsync(CancellationToken cancellationToken = default) - { - if (Transaction != null) - { - await Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - } - } - - /// - /// Rollbacks the transaction using the specified cancellation token - /// - /// The cancellation token - public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) - { - if (Transaction != null) - { - await Transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); - } - } - - /// - public async Task> ExecuteRawSqlAsync(string sql, Func map, - Action config = null, CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - - await using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sql, command, configuration); - - await Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); - await using var result = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); - - var items = new List(); - while (await result.ReadAsync(cancellationToken).ConfigureAwait(false)) - { - items.Add(map(result)); - } - - return items; - } - - /// - /// Executes the non query using the specified sql command - /// - /// The sql command - /// The configuration. - /// The int - public int ExecuteNonQuery(string sqlCommand, Action config = null) - { - var configuration = GetConfig(config); - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - Context.Database.OpenConnection(); - return command.ExecuteNonQuery(); - } - - /// - /// Executes the function using the specified name - /// - /// The result - /// The name - /// The configuration. - /// The result - public TResult ExecuteFunction(string name, Action config = null) where TResult : class - { - var configuration = GetConfig(config); - var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set() - .FromSqlRaw(sql, GetParameterValues(configuration)) - .FirstOrDefault(); - } - - /// - /// Executes the command using the specified command timeout - /// - /// The sql command - /// The configuration. - /// The int - public int ExecuteCommand(string sqlCommand, Action config = null) - { - var configuration = GetConfig(config); - - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - Context.Database.OpenConnection(); - var result = command.ExecuteScalar(); - - return result == DBNull.Value ? 0 : Convert.ToInt32(result); - } - - /// - /// Executes the command using the specified command timeout - /// - /// The sql command - /// - /// The cancellation token - /// A task containing the int - public async Task ExecuteCommandAsync(string sqlCommand, - Action config = null, CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - - await using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - await Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); - var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); - - return result == DBNull.Value ? 0 : Convert.ToInt32(result); - } - - /// - /// Executes the function using the specified name - /// - /// The result - /// The name - /// - /// The cancellation token - /// A task containing the result - public Task ExecuteFunctionAsync(string name, - Action config = null, - CancellationToken cancellationToken = default) where TResult : class - { - var configuration = GetConfig(config); - var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)) - .FirstOrDefaultAsync(cancellationToken); - } - - /// - /// Executes the procedure using the specified name - /// - /// The name - /// The configuration - /// The int - public int ExecuteProcedure(string name, Action config = null) - { - var configuration = GetConfig(config); - return ExecuteCommand(GetQueryProcedure(name, configuration) + ";", config); - } - - /// - /// Executes the query using the specified sql query - /// - /// The entity - /// The sql query - /// The configuration. - /// An enumerable of t entity - public IEnumerable ExecuteQuery(string sqlQuery, Action config = null) where TEntity : class - { - var configuration = GetConfig(config); - var sql = ParseSql(sqlQuery, configuration); - return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)); - } - - /// - /// Executes the transaction using the specified operation - /// - /// The operation - /// - public void ExecuteTransaction(Action operation) - { - if (operation == null) - { - throw new ArgumentNullException(nameof(operation)); - } - - var strategy = Context.Database.CreateExecutionStrategy(); - - strategy.Execute(() => { operation.Invoke((ISqlUnitOfWork)this); }); - } - - /// - /// Executes the procedure using the specified name - /// - /// The name - /// - /// The cancellation token - /// A task containing the int - public Task ExecuteProcedureAsync(string name, Action config = null, - CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - return ExecuteCommandAsync(GetQueryProcedure(name, configuration) + ";", config, cancellationToken); - } - - /// - public Task ExecuteTransactionAsync(Func operation, - CancellationToken cancellationToken = default) - { - if (operation == null) - { - throw new ArgumentNullException(nameof(operation)); - } - - var strategy = Context.Database.CreateExecutionStrategy(); - - return strategy.ExecuteAsync(() => operation.Invoke((ISqlUnitOfWork)this)); - } - - /// - /// Begins the transaction - /// - public void BeginTransaction() - { - Transaction?.Dispose(); - Transaction = Context.Database.BeginTransaction(); - } - - /// - /// Commits the transaction - /// - public virtual void CommitTransaction() - { - Transaction?.Commit(); - } - - /// - /// Executes the raw sql using the specified sql - /// - /// The - /// The sql - /// The map - /// The configuration. - /// The items - public IEnumerable ExecuteRawSql(string sql, Func map, - Action config = null) - { - var configuration = GetConfig(config); - - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sql, command, configuration); - - Context.Database.OpenConnection(); - using var result = command.ExecuteReader(); - - var items = new List(); - while (result.Read()) - { - items.Add(map(result)); - } - - return items; - } - - /// - /// Gets the transaction - /// - /// The db transaction - public DbTransaction GetTransaction() - { - return Transaction?.GetDbTransaction(); - } - - /// - /// Rollbacks the transaction - /// - public void RollbackTransaction() - { - Transaction?.Rollback(); - } - - /// - /// Uses the transaction using the specified transaction - /// - /// The transaction - public void UseTransaction(DbTransaction transaction) - { - Context.Database.UseTransaction(transaction); - } - - /// - /// Uses the transaction using the specified transaction - /// - /// The transaction - /// The cancellation token - public Task UseTransactionAsync(DbTransaction transaction, CancellationToken cancellationToken = default) - { - return Context.Database.UseTransactionAsync(transaction, cancellationToken); - } - - /// - /// Gets the query function using the specified name. Parameter values are emitted as - /// positional placeholders ({0}, {1}, …) so the values travel as - /// s through FromSqlRaw and are never - /// interpolated into the SQL text. - /// - /// The name - /// The configuration. - /// The string - internal static string GetQueryFunction(string name, SqlConfiguration config) - { - return $"SELECT {name}({GetPositionalPlaceholders(config.Parameters.Count)} )"; - } - - /// - /// Gets the query procedure using the specified name. MySQL invokes stored procedures with - /// CALL; parameter values are emitted as named placeholders matching the - /// s created by , so the values are never - /// interpolated into the SQL text. - /// - /// The name - /// The configuration. - /// The string - internal static string GetQueryProcedure(string name, SqlConfiguration config) - { - return $"CALL {name}({GetNamedPlaceholders(config.Parameters.ToArray())} )"; - } - - /// - /// Builds a comma-separated list of positional placeholders ({0}, {1}, …) for the - /// given parameter count. Used by FromSqlRaw, which substitutes each placeholder with a - /// parameter reference. - /// - /// The number of parameters. - /// The placeholder string. - internal static string GetPositionalPlaceholders(int count) - { - var cmdBuilder = new StringBuilder(); - for (var i = 0; i < count; i++) - { - if (i > 0) - { - cmdBuilder.Append(','); - } - - cmdBuilder.Append(" {").Append(i).Append('}'); - } - - return cmdBuilder.ToString(); - } - - /// - /// Builds a comma-separated list of named placeholders (@Param0, @Name, …) using - /// the same naming convention as , so the placeholders bind to the - /// parameters added to the command. - /// - /// The parameters - /// The placeholder string. - internal static string GetNamedPlaceholders(params ParamValue[] parameters) - { - var cmdBuilder = new StringBuilder(); - if (parameters is not { Length: > 0 }) - { - return cmdBuilder.ToString(); - } - - for (var i = 0; i < parameters.Length; i++) - { - if (i > 0) - { - cmdBuilder.Append(','); - } - - var parameterName = string.IsNullOrEmpty(parameters[i].Name) ? $"Param{i}" : parameters[i].Name; - cmdBuilder.Append(" @").Append(parameterName); - } - - return cmdBuilder.ToString(); - } - - /// - /// Gets the ordered parameter values used to feed FromSqlRaw's positional placeholders. - /// - /// The configuration. - /// The parameter values, in the same order as the emitted placeholders. - internal static object[] GetParameterValues(SqlConfiguration config) - { - return config.Parameters == null - ? Array.Empty() - : config.Parameters.Select(p => p.Value).ToArray(); - } - - private static string ParseSql(string sql, SqlConfiguration config) - { - return !string.IsNullOrEmpty(config.Tag) ? GetQueryWithTag(sql, config.Tag) : sql; - } - /// - /// Sets the command using the specified command timeout - /// - /// The sql command - /// The command - /// The configuration. - private void SetCommand(string sqlCommand, DbCommand command, SqlConfiguration config) - { - if (Transaction != null) - { - command.Transaction = Transaction.GetDbTransaction(); - } - - command.CommandText = ParseSql(sqlCommand, config); - command.CommandType = CommandType.Text; - command.CommandTimeout = config.GetCommandTimeout(Context); - - if (config.Parameters == null) - { - return; - } - - var i = 0; - foreach (var t in config.Parameters) - { - var parameterName = string.IsNullOrEmpty(t.Name) ? $"Param{i}" : t.Name; - var parameter = command.CreateParameter(); - parameter.ParameterName = parameterName; - parameter.Value = t.Value; - command.Parameters.Add(parameter); - i++; - } - } - - /// - /// Gets the query with tag using the specified query - /// - /// The query - /// The tag - /// The string - private static string GetQueryWithTag(string query, string tag) - { - var queryBuilder = new StringBuilder(); - queryBuilder.AppendLine($"--{tag}"); - queryBuilder.AppendLine(); - queryBuilder.Append(query); - return queryBuilder.ToString(); - } - - private static SqlConfiguration GetConfig(Action config = null) - { - var configuration = new DefaultSqlConfiguration(); - config?.Invoke(configuration); - - return configuration; - } - - /// - /// Disposes this instance - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes the disposing - /// - /// The disposing - protected virtual void Dispose(bool disposing) - { - if (Disposed) - { - return; - } - - if (disposing) - { - Transaction?.Dispose(); - Context?.Dispose(); - } - - Disposed = true; - } -} diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs index 47b7968..9e70c91 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs @@ -1,266 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Options; -using eQuantic.Core.Data.Repository.Sql; +using System; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace eQuantic.Core.Data.EntityFramework.MySql.Repository; -public abstract class UnitOfWork : SqlExecutor -{ - protected static int IsMigrating; - - protected UnitOfWork(DbContext context) : base(context) - { - } - - internal DbContext GetDbContext() => Context; -} - -public abstract class UnitOfWork : UnitOfWork, ISqlUnitOfWork +/// +/// MySQL unit of work. The implementation lives in ; +/// MySQL uses the default ANSI CALL stored-procedure dialect, so no dialect override is +/// required. +/// +public abstract class UnitOfWork : RelationalUnitOfWork where TDbContext : DbContext { - private readonly TDbContext _context; - private readonly IServiceProvider _serviceProvider; - - protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(context) - { - _serviceProvider = serviceProvider; - _context = context; - } - - public int Commit() - { - return _context.SaveChanges(); - } - - public int CommitAndRefreshChanges() - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = _context.SaveChanges(); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAsync(CancellationToken cancellationToken = default) - { - return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public int Commit(Action options) - { - return Commit(); - } - - public int CommitAndRefreshChanges(Action options) - { - return CommitAndRefreshChanges(); - } - - public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAndRefreshChangesAsync(cancellationToken); - } - - public Task CommitAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAsync(cancellationToken); - } - - Data.Repository.ISet IQueryableUnitOfWork.CreateSet() => InternalCreateSet(); - - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).ApplyCurrentValues(original, current); - } - - public void Attach(TEntity item) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).Attach(item); - } - - public IEnumerable GetPendingMigrations() - { - return _context.Database.GetPendingMigrations(); - } - - public void LoadProperty(TEntity item, Expression> selector) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - ((Set)InternalCreateSet()).LoadProperty(item, selector); - } - - public void LoadProperty(TEntity item, string propertyName) - where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).LoadProperty(item, propertyName); - } - - public Task LoadPropertyAsync(TEntity item, Expression> selector, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, selector, cancellationToken); - } - - public Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, propertyName, cancellationToken); - } - - public void LoadCollection(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - _context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); - } - else - { - _context.Entry(item).Collection(navigationProperty).Load(); - } - } - - public async Task LoadCollectionAsync(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); - } - else - { - await _context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); - } - } - - public void Reload(TEntity item) where TEntity : class - { - var entry = _context.Entry(item); - entry.CurrentValues.SetValues(entry.OriginalValues); - entry.Reload(); - } - - public void RollbackChanges() - { - // set all entities in change tracker - // as 'unchanged state' - _context?.ChangeTracker.Entries() - .ToList() - .ForEach(entry => entry.State = EntityState.Unchanged); - } - - public void SetModified(TEntity item) where TEntity : class - { - //this operation also attach item in object state manager - _context.Entry(item).State = EntityState.Modified; - } - - public void UpdateDatabase() - { - if (0 != Interlocked.Exchange(ref IsMigrating, 1)) - { - return; - } - - try - { - _context.Database.Migrate(); - } - finally - { - Interlocked.Exchange(ref IsMigrating, 0); - } - } - - public virtual Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => - InternalCreateSet(); - - public virtual SaveOptions GetSaveOptions() - { - return new SaveOptions(); - } - - public virtual IRepository GetRepository() - where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork - { - var repo = _serviceProvider.GetRequiredService>(); - return repo; - } - - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(serviceProvider, context) { - return _serviceProvider.GetRequiredService>(); } - - private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => - new Set(_context); } diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj index 7277f70..67f3708 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj @@ -64,6 +64,8 @@ + Include="..\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.csproj" /> + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj index de585a9..2aa1dc3 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj @@ -64,6 +64,8 @@ + Include="..\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.csproj" /> + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj index 0df09ac..0cdc4fd 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj @@ -64,6 +64,8 @@ + Include="..\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.csproj" /> + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj index 7b01e0c..afa8549 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj @@ -100,5 +100,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/ExpressionConverter.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/ExpressionConverter.cs deleted file mode 100644 index a121d43..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/ExpressionConverter.cs +++ /dev/null @@ -1,176 +0,0 @@ -#if NET7_0_OR_GREATER && !NET10_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Query; - -namespace eQuantic.Core.Data.EntityFramework.PostgreSql; - -internal class ExpressionConverter -{ - public static Expression, SetPropertyCalls>> ConvertExpression( - Expression> updateExpression) - { - ArgumentNullException.ThrowIfNull(updateExpression); - - var parameter = Expression.Parameter(typeof(SetPropertyCalls), "e"); - var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); - var block = methodCalls.Last(); - - return Expression.Lambda, SetPropertyCalls>>(block, parameter); - } - - private class ExpressionRewriter(ParameterExpression parameter) : ExpressionVisitor - { - private readonly List _setPropertyCalls = []; - - public IEnumerable Rewrite(Expression body) - { - if (body is not MemberInitExpression memberInit) - throw new NotSupportedException("Only MemberInit expressions are supported."); - - MethodCallExpression currentExpression = null; - foreach (var binding in memberInit.Bindings.OfType()) - { - currentExpression = RewriteBinding(binding, currentExpression); - } - - return _setPropertyCalls; - } - - private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCallExpression callExpression = null) - { - var propertyInfo = (PropertyInfo)binding.Member; - var propertyType = propertyInfo.PropertyType; - var entityParameter = Expression.Parameter(typeof(T), "entity"); - var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); - var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - var setPropertyMethod = typeof(SetPropertyCalls) - .GetMethods() - .Where(m => m.Name == nameof(SetPropertyCalls.SetProperty) && m.IsGenericMethod) - .Single(m => - { - var parameters = m.GetParameters(); - var genericArgs = m.GetGenericArguments(); - return genericArgs.Length == 1 && - parameters.Length == 2 && - IsGenericFunc(parameters[0].ParameterType) && - parameters[1].ParameterType == genericArgs[0]; - }) - .MakeGenericMethod(propertyType); - - var setPropertyCall = Expression.Call( - callExpression != null ? callExpression.Reduce() : parameter, - setPropertyMethod, - propertyLambda, - binding.Expression - ); - - _setPropertyCalls.Add(setPropertyCall); - - return setPropertyCall; - } - - private static bool IsGenericFunc(Type type) - { - return type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(Func<,>); - } - } -} -#endif -#if NET10_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Query; - -namespace eQuantic.Core.Data.EntityFramework.PostgreSql; - -internal class ExpressionConverter -{ - public static Action> ConvertExpression( - Expression> updateExpression) - { - ArgumentNullException.ThrowIfNull(updateExpression); - - var parameter = Expression.Parameter(typeof(UpdateSettersBuilder), "e"); - var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); - var lastCall = methodCalls.Last(); - var body = lastCall.Type == typeof(void) - ? lastCall - : Expression.Block(typeof(void), lastCall); - return Expression.Lambda>>(body, parameter).Compile(); - } - - private class ExpressionRewriter(ParameterExpression parameter) : ExpressionVisitor - { - private readonly List _setPropertyCalls = []; - - public IEnumerable Rewrite(Expression body) - { - if (body is not MemberInitExpression memberInit) - throw new NotSupportedException("Only MemberInit expressions are supported."); - - MethodCallExpression currentExpression = null; - foreach (var binding in memberInit.Bindings.OfType()) - { - currentExpression = RewriteBinding(binding, currentExpression); - } - - return _setPropertyCalls; - } - - private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCallExpression callExpression = null) - { - var propertyInfo = (PropertyInfo)binding.Member; - var propertyType = propertyInfo.PropertyType; - - var entityParameter = Expression.Parameter(typeof(T), "entity"); - var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); - var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - - var setPropertyMethod = typeof(UpdateSettersBuilder) - .GetMethods() - .Where(m => m.Name == nameof(UpdateSettersBuilder.SetProperty) && m.IsGenericMethod) - .Single(m => - { - var parameters = m.GetParameters(); - var genericArgs = m.GetGenericArguments(); - return genericArgs.Length == 1 && - parameters.Length == 2 && - IsExpressionFunc(parameters[0].ParameterType) && - parameters[1].ParameterType == genericArgs[0]; - }) - .MakeGenericMethod(propertyType); - - var setPropertyCall = Expression.Call( - callExpression != null ? callExpression : parameter, - setPropertyMethod, - propertyLambda, - binding.Expression - ); - - _setPropertyCalls.Add(setPropertyCall); - - return setPropertyCall; - } - - private static bool IsExpressionFunc(Type type) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Expression<>)) - { - var delegateType = type.GetGenericArguments()[0]; - - return delegateType.IsGenericType && - delegateType.GetGenericTypeDefinition() == typeof(Func<,>); - } - return false; - } - } -} -#endif \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs index 562fabb..ad125df 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs @@ -1,268 +1,16 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; namespace eQuantic.Core.Data.EntityFramework.PostgreSql.Repository; -public class Set : SetBase where TEntity : class, IEntity, new() +/// +/// PostgreSQL entity set. The implementation lives in ; this +/// type is preserved for source compatibility. +/// +public class Set : RelationalSet where TEntity : class, IEntity, new() { public Set(DbContext context) : base(context) { } - - public override long DeleteMany(Expression> filter) - { - return InternalDbSet.Where(filter).ExecuteDelete(); - } - - public override async Task DeleteManyAsync(Expression> filter, - CancellationToken cancellationToken = default) - { - return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - } - - public void LoadCollection(TChildEntity item, - Expression>> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Collection(selector).Load(); - } - - public void LoadCollection(TChildEntity item, string propertyName) - where TChildEntity : class - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - - public async Task LoadCollectionAsync(TChildEntity item, - Expression>> selector) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Collection(selector).LoadAsync().ConfigureAwait(false); - } - - public async Task LoadCollectionAsync(TChildEntity item, string propertyName) - where TChildEntity : class - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync().ConfigureAwait(false); - } - - public void LoadProperties(TEntity entity, params string[] properties) - { - if (properties is not { Length: > 0 }) - { - return; - } - - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - LoadProperty(entity, property); - } - else - { - LoadCascade(props, entity); - } - } - } - - public async Task LoadPropertiesAsync(TEntity entity, params string[] properties) - { - if (properties is { Length: > 0 }) - { - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - await LoadPropertyAsync(entity, property).ConfigureAwait(false); - } - else - { - await LoadCascadeAsync(props, entity).ConfigureAwait(false); - } - } - } - } - - public void LoadProperty(TChildEntity item, - Expression> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Reference(selector).Load(); - } - - public void LoadProperty(TChildEntity item, string propertyName) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - else - { - DbContext.Entry(item).Reference(propertyName).Load(); - } - } - - public async Task LoadPropertyAsync(TChildEntity item, - Expression> selector, CancellationToken cancellationToken = default) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task LoadPropertyAsync(TChildEntity item, string propertyName, - CancellationToken cancellationToken = default) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); - } - else - { - await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); - } - } - - public override long UpdateMany(Expression> filter, - Expression> updateExpression) - { - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return InternalDbSet.Where(filter).ExecuteUpdate(convertedExpression); - } - - public override async Task UpdateManyAsync(Expression> filter, - Expression> updateExpression, CancellationToken cancellationToken = default) - { - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken).ConfigureAwait(false); - } - - private void LoadCascade(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - LoadProperty(obj, props[index]); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - LoadCascade(props, nextObj, index + 1); - } - } - - private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - await LoadPropertyAsync(obj, props[index]).ConfigureAwait(false); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - await LoadCascadeAsync(props, nextObj, index + 1).ConfigureAwait(false); - } - } - - internal Expression> GetExpression(TKey id) - { - return DbContext.GetFindByKeyExpression(id); - } - - public override IQueryable GetQueryable(Action configuration, - Func, IQueryable> internalQueryAction) - { - if (configuration == null) - { - return internalQueryAction.Invoke(this); - } - - var config = GetConfig(configuration); - var queryableConfig = config as QueryableConfiguration; - - var query = string.IsNullOrEmpty(queryableConfig?.SqlRaw) ? this : InternalDbSet.FromSqlRaw(queryableConfig.SqlRaw); - - if (config.HasNoTracking) - { - query = query.AsNoTracking(); - } - - if (config.Properties?.Any() == true) - { - query = query.IncludeMany(config.Properties.ToArray()); - } - - if (queryableConfig?.IgnoreQueryFilters == true) - { - query = query.IgnoreQueryFilters(); - } - - if (!string.IsNullOrEmpty(config.Tag)) - { - query = query.TagWith(config.Tag); - } - - if (queryableConfig != null) - { - query = queryableConfig.BeforeCustomization.Invoke(query); - } - - query = internalQueryAction.Invoke(query); - - if (config.SortingColumns.Any()) - { - query = query.OrderBy(config.SortingColumns.ToArray()); - } - - if (queryableConfig != null) - { - query = queryableConfig.AfterCustomization.Invoke(query); - } - - return query; - } } diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs index d704c47..92f5b7e 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs @@ -1,266 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Options; -using eQuantic.Core.Data.Repository.Sql; +using System; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace eQuantic.Core.Data.EntityFramework.PostgreSql.Repository; -public abstract class UnitOfWork : SqlExecutor -{ - protected static int IsMigrating; - - protected UnitOfWork(DbContext context) : base(context) - { - } - - internal DbContext GetDbContext() => Context; -} - -public abstract class UnitOfWork : UnitOfWork, ISqlUnitOfWork +/// +/// PostgreSQL unit of work. The implementation lives in +/// ; PostgreSQL uses the default ANSI CALL +/// stored-procedure dialect, so no dialect override is required. +/// +public abstract class UnitOfWork : RelationalUnitOfWork where TDbContext : DbContext { - private readonly TDbContext _context; - private readonly IServiceProvider _serviceProvider; - - protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(context) - { - _serviceProvider = serviceProvider; - _context = context; - } - - public int Commit() - { - return _context.SaveChanges(); - } - - public int CommitAndRefreshChanges() - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = _context.SaveChanges(); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAsync(CancellationToken cancellationToken = default) - { - return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public int Commit(Action options) - { - return Commit(); - } - - public int CommitAndRefreshChanges(Action options) - { - return CommitAndRefreshChanges(); - } - - public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAndRefreshChangesAsync(cancellationToken); - } - - public Task CommitAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAsync(cancellationToken); - } - - Data.Repository.ISet IQueryableUnitOfWork.CreateSet() => InternalCreateSet(); - - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).ApplyCurrentValues(original, current); - } - - public void Attach(TEntity item) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).Attach(item); - } - - public IEnumerable GetPendingMigrations() - { - return _context.Database.GetPendingMigrations(); - } - - public void LoadProperty(TEntity item, Expression> selector) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - ((Set)InternalCreateSet()).LoadProperty(item, selector); - } - - public void LoadProperty(TEntity item, string propertyName) - where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).LoadProperty(item, propertyName); - } - - public Task LoadPropertyAsync(TEntity item, Expression> selector, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, selector, cancellationToken); - } - - public Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, propertyName, cancellationToken); - } - - public void LoadCollection(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - _context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); - } - else - { - _context.Entry(item).Collection(navigationProperty).Load(); - } - } - - public async Task LoadCollectionAsync(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); - } - else - { - await _context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); - } - } - - public void Reload(TEntity item) where TEntity : class - { - var entry = _context.Entry(item); - entry.CurrentValues.SetValues(entry.OriginalValues); - entry.Reload(); - } - - public void RollbackChanges() - { - // set all entities in change tracker - // as 'unchanged state' - _context?.ChangeTracker.Entries() - .ToList() - .ForEach(entry => entry.State = EntityState.Unchanged); - } - - public void SetModified(TEntity item) where TEntity : class - { - //this operation also attach item in object state manager - _context.Entry(item).State = EntityState.Modified; - } - - public void UpdateDatabase() - { - if (0 != Interlocked.Exchange(ref IsMigrating, 1)) - { - return; - } - - try - { - _context.Database.Migrate(); - } - finally - { - Interlocked.Exchange(ref IsMigrating, 0); - } - } - - public virtual Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => - InternalCreateSet(); - - public virtual SaveOptions GetSaveOptions() - { - return new SaveOptions(); - } - - public virtual IRepository GetRepository() - where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork - { - var repo = _serviceProvider.GetRequiredService>(); - return repo; - } - - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(serviceProvider, context) { - return _serviceProvider.GetRequiredService>(); } - - private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => - new Set(_context); } diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj index e54452c..8a566ad 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj @@ -65,5 +65,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj index 445e528..b7aff6e 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj @@ -65,5 +65,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj index 09ce817..8258b3f 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj @@ -65,5 +65,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj index ffb4690..50b973c 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj @@ -98,5 +98,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/ExpressionConverter.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/ExpressionConverter.cs similarity index 93% rename from src/eQuantic.Core.Data.EntityFramework.MySql/ExpressionConverter.cs rename to src/eQuantic.Core.Data.EntityFramework.Relational/ExpressionConverter.cs index 4e0c57a..bbde74d 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/ExpressionConverter.cs +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/ExpressionConverter.cs @@ -1,4 +1,4 @@ -#if NET7_0_OR_GREATER && !NET10_0_OR_GREATER +#if NET7_0_OR_GREATER && !NET10_0_OR_GREATER using System; using System.Collections.Generic; using System.Linq; @@ -6,7 +6,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore.Query; -namespace eQuantic.Core.Data.EntityFramework.MySql; +namespace eQuantic.Core.Data.EntityFramework.Relational; internal class ExpressionConverter { @@ -18,7 +18,7 @@ public static Expression, SetPropertyCalls), "e"); var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); var block = methodCalls.Last(); - + return Expression.Lambda, SetPropertyCalls>>(block, parameter); } @@ -60,7 +60,7 @@ private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCall parameters[1].ParameterType == genericArgs[0]; }) .MakeGenericMethod(propertyType); - + var setPropertyCall = Expression.Call( callExpression != null ? callExpression.Reduce() : parameter, setPropertyMethod, @@ -69,7 +69,7 @@ private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCall ); _setPropertyCalls.Add(setPropertyCall); - + return setPropertyCall; } @@ -89,7 +89,7 @@ private static bool IsGenericFunc(Type type) using System.Reflection; using Microsoft.EntityFrameworkCore.Query; -namespace eQuantic.Core.Data.EntityFramework.MySql; +namespace eQuantic.Core.Data.EntityFramework.Relational; internal class ExpressionConverter { @@ -101,8 +101,8 @@ public static Action> ConvertExpression( var parameter = Expression.Parameter(typeof(UpdateSettersBuilder), "e"); var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); var lastCall = methodCalls.Last(); - var body = lastCall.Type == typeof(void) - ? lastCall + var body = lastCall.Type == typeof(void) + ? lastCall : Expression.Block(typeof(void), lastCall); return Expression.Lambda>>(body, parameter).Compile(); } @@ -129,25 +129,25 @@ private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCall { var propertyInfo = (PropertyInfo)binding.Member; var propertyType = propertyInfo.PropertyType; - + var entityParameter = Expression.Parameter(typeof(T), "entity"); var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - + var setPropertyMethod = typeof(UpdateSettersBuilder) .GetMethods() .Where(m => m.Name == nameof(UpdateSettersBuilder.SetProperty) && m.IsGenericMethod) - .Single(m => + .Single(m => { var parameters = m.GetParameters(); var genericArgs = m.GetGenericArguments(); return genericArgs.Length == 1 && - parameters.Length == 2 && + parameters.Length == 2 && IsExpressionFunc(parameters[0].ParameterType) && parameters[1].ParameterType == genericArgs[0]; }) .MakeGenericMethod(propertyType); - + var setPropertyCall = Expression.Call( callExpression != null ? callExpression : parameter, setPropertyMethod, @@ -156,7 +156,7 @@ private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCall ); _setPropertyCalls.Add(setPropertyCall); - + return setPropertyCall; } @@ -165,12 +165,12 @@ private static bool IsExpressionFunc(Type type) if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Expression<>)) { var delegateType = type.GetGenericArguments()[0]; - - return delegateType.IsGenericType && + + return delegateType.IsGenericType && delegateType.GetGenericTypeDefinition() == typeof(Func<,>); } return false; } } } -#endif \ No newline at end of file +#endif diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs new file mode 100644 index 0000000..b1583bd --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs @@ -0,0 +1,14 @@ +using eQuantic.Core.Data.Repository.Config; +using Microsoft.EntityFrameworkCore; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; + +internal static class SqlConfigurationExtensions +{ + private const int DefaultCommandTimeout = 60; + + public static int GetCommandTimeout(this SqlConfiguration config, DbContext context) + { + return config?.CommandTimeout ?? context?.Database.GetCommandTimeout() ?? DefaultCommandTimeout; + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs new file mode 100644 index 0000000..b56faa3 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.EntityFramework.Repository; +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Config; +using eQuantic.Linq.Extensions; +using Microsoft.EntityFrameworkCore; +#if NET6_0 || NETSTANDARD2_1 +using Z.EntityFramework.Plus; +#endif + +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository; + +/// +/// The shared relational entity set used by the SqlServer, PostgreSql and MySql providers. +/// +public class RelationalSet : SetBase where TEntity : class, IEntity, new() +{ + public RelationalSet(DbContext context) : base(context) + { + } + + public override long DeleteMany(Expression> filter) + { +#if NET6_0 || NETSTANDARD2_1 + return InternalDbSet.Where(filter).Delete(); +#else + return InternalDbSet.Where(filter).ExecuteDelete(); +#endif + } + + public override async Task DeleteManyAsync(Expression> filter, + CancellationToken cancellationToken = default) + { +#if NET6_0 || NETSTANDARD2_1 + return await InternalDbSet.Where(filter).DeleteAsync(cancellationToken).ConfigureAwait(false); +#else + return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); +#endif + } + + public void LoadCollection(TChildEntity item, + Expression>> selector) + where TChildEntity : class + where TComplexProperty : class + { + DbContext.Entry(item).Collection(selector).Load(); + } + + public void LoadCollection(TChildEntity item, string propertyName) + where TChildEntity : class + { + DbContext.Entry(item).Collection(propertyName).Load(); + } + + public async Task LoadCollectionAsync(TChildEntity item, + Expression>> selector) + where TChildEntity : class where TComplexProperty : class + { + await DbContext.Entry(item).Collection(selector).LoadAsync().ConfigureAwait(false); + } + + public async Task LoadCollectionAsync(TChildEntity item, string propertyName) + where TChildEntity : class + { + await DbContext.Entry(item).Collection(propertyName).LoadAsync().ConfigureAwait(false); + } + + public void LoadProperties(TEntity entity, params string[] properties) + { + if (properties is not { Length: > 0 }) + { + return; + } + + foreach (var property in properties) + { + if (string.IsNullOrEmpty(property)) + { + continue; + } + + var props = property.Split('.'); + + if (props.Length == 1) + { + LoadProperty(entity, property); + } + else + { + LoadCascade(props, entity); + } + } + } + + public async Task LoadPropertiesAsync(TEntity entity, params string[] properties) + { + if (properties is { Length: > 0 }) + { + foreach (var property in properties) + { + if (string.IsNullOrEmpty(property)) + { + continue; + } + + var props = property.Split('.'); + + if (props.Length == 1) + { + await LoadPropertyAsync(entity, property).ConfigureAwait(false); + } + else + { + await LoadCascadeAsync(props, entity).ConfigureAwait(false); + } + } + } + } + + public void LoadProperty(TChildEntity item, + Expression> selector) + where TChildEntity : class + where TComplexProperty : class + { + DbContext.Entry(item).Reference(selector).Load(); + } + + public void LoadProperty(TChildEntity item, string propertyName) + where TChildEntity : class + { + if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) + { + DbContext.Entry(item).Collection(propertyName).Load(); + } + else + { + DbContext.Entry(item).Reference(propertyName).Load(); + } + } + + public async Task LoadPropertyAsync(TChildEntity item, + Expression> selector, CancellationToken cancellationToken = default) + where TChildEntity : class where TComplexProperty : class + { + await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task LoadPropertyAsync(TChildEntity item, string propertyName, + CancellationToken cancellationToken = default) + where TChildEntity : class + { + if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) + { + await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); + } + else + { + await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); + } + } + + public override long UpdateMany(Expression> filter, + Expression> updateExpression) + { +#if NET6_0 || NETSTANDARD2_1 + return InternalDbSet.Where(filter).Update(updateExpression); +#else + var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); + return InternalDbSet.Where(filter).ExecuteUpdate(convertedExpression); +#endif + } + + public override async Task UpdateManyAsync(Expression> filter, + Expression> updateExpression, CancellationToken cancellationToken = default) + { +#if NET6_0 || NETSTANDARD2_1 + return await InternalDbSet.Where(filter).UpdateAsync(updateExpression, cancellationToken).ConfigureAwait(false); +#else + var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); + return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken).ConfigureAwait(false); +#endif + } + + private void LoadCascade(string[] props, object obj, int index = 0) + { + if (obj == null) + { + return; + } + + var prop = obj.GetType().GetProperty(props[index]); + var nextObj = prop?.GetValue(obj); + if (nextObj == null) + { + LoadProperty(obj, props[index]); + nextObj = prop?.GetValue(obj); + } + + if (props.Length > index + 1) + { + LoadCascade(props, nextObj, index + 1); + } + } + + private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) + { + if (obj == null) + { + return; + } + + var prop = obj.GetType().GetProperty(props[index]); + var nextObj = prop?.GetValue(obj); + if (nextObj == null) + { + await LoadPropertyAsync(obj, props[index]).ConfigureAwait(false); + nextObj = prop?.GetValue(obj); + } + + if (props.Length > index + 1) + { + await LoadCascadeAsync(props, nextObj, index + 1).ConfigureAwait(false); + } + } + + internal Expression> GetExpression(TKey id) + { + return DbContext.GetFindByKeyExpression(id); + } + + public override IQueryable GetQueryable(Action configuration, + Func, IQueryable> internalQueryAction) + { + if (configuration == null) + { + return internalQueryAction.Invoke(this); + } + + var config = GetConfig(configuration); + var queryableConfig = config as QueryableConfiguration; + + var query = string.IsNullOrEmpty(queryableConfig?.SqlRaw) ? this : InternalDbSet.FromSqlRaw(queryableConfig.SqlRaw); + + if (config.HasNoTracking) + { + query = query.AsNoTracking(); + } + + if (config.Properties?.Any() == true) + { + query = query.IncludeMany(config.Properties.ToArray()); + } + + if (queryableConfig?.IgnoreQueryFilters == true) + { + query = query.IgnoreQueryFilters(); + } + + if (!string.IsNullOrEmpty(config.Tag)) + { + query = query.TagWith(config.Tag); + } + + if (queryableConfig != null) + { + query = queryableConfig.BeforeCustomization.Invoke(query); + } + + query = internalQueryAction.Invoke(query); + + if (config.SortingColumns.Any()) + { + query = query.OrderBy(config.SortingColumns.ToArray()); + } + + if (queryableConfig != null) + { + query = queryableConfig.AfterCustomization.Invoke(query); + } + + return query; + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs similarity index 92% rename from src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs rename to src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs index 4a919ba..5db22d6 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Data.Common; @@ -7,22 +7,24 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.PostgreSql.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; using eQuantic.Core.Data.Repository.Config; using eQuantic.Core.Data.Repository.Sql; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -namespace eQuantic.Core.Data.EntityFramework.PostgreSql.Repository; +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository; /// -/// The sql executor class +/// The shared relational sql executor. Providers reuse this implementation and only override the +/// dialect-specific when needed (e.g. SQL Server uses EXEC +/// whereas PostgreSQL/MySQL use the ANSI CALL). /// /// /// /// [ExcludeFromCodeCoverage] -public abstract class SqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable +public abstract class RelationalSqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable { /// /// The context @@ -38,12 +40,12 @@ public abstract class SqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable /// The transaction /// protected IDbContextTransaction Transaction; - + /// /// Initializes a new instance of the class /// /// The context - protected SqlExecutor(DbContext context) + protected RelationalSqlExecutor(DbContext context) { Context = context; } @@ -69,7 +71,7 @@ public virtual async Task CommitTransactionAsync(CancellationToken cancellationT await Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); } } - + /// /// Rollbacks the transaction using the specified cancellation token /// @@ -81,13 +83,13 @@ public async Task RollbackTransactionAsync(CancellationToken cancellationToken = await Transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); } } - + /// public async Task> ExecuteRawSqlAsync(string sql, Func map, Action config = null, CancellationToken cancellationToken = default) { var configuration = GetConfig(config); - + await using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sql, command, configuration); @@ -144,7 +146,7 @@ public TResult ExecuteFunction(string name, Action config = null) { var configuration = GetConfig(config); - + using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sqlCommand, command, configuration); @@ -153,7 +155,7 @@ public int ExecuteCommand(string sqlCommand, Action con return result == DBNull.Value ? 0 : Convert.ToInt32(result); } - + /// /// Executes the command using the specified command timeout /// @@ -165,7 +167,7 @@ public async Task ExecuteCommandAsync(string sqlCommand, Action config = null, CancellationToken cancellationToken = default) { var configuration = GetConfig(config); - + await using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sqlCommand, command, configuration); @@ -183,7 +185,7 @@ public async Task ExecuteCommandAsync(string sqlCommand, /// /// The cancellation token /// A task containing the result - public Task ExecuteFunctionAsync(string name, + public Task ExecuteFunctionAsync(string name, Action config = null, CancellationToken cancellationToken = default) where TResult : class { @@ -192,7 +194,7 @@ public Task ExecuteFunctionAsync(string name, return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)) .FirstOrDefaultAsync(cancellationToken); } - + /// /// Executes the procedure using the specified name /// @@ -202,9 +204,9 @@ public Task ExecuteFunctionAsync(string name, public int ExecuteProcedure(string name, Action config = null) { var configuration = GetConfig(config); - return ExecuteCommand(GetQueryProcedure(name, configuration) + ";", config); + return ExecuteCommand(BuildProcedureSql(name, configuration) + ";", config); } - + /// /// Executes the query using the specified sql query /// @@ -218,7 +220,7 @@ public IEnumerable ExecuteQuery(string sqlQuery, Action().FromSqlRaw(sql, GetParameterValues(configuration)); } - + /// /// Executes the transaction using the specified operation /// @@ -247,7 +249,7 @@ public Task ExecuteProcedureAsync(string name, Action @@ -293,7 +295,7 @@ public IEnumerable ExecuteRawSql(string sql, Func map, Action config = null) { var configuration = GetConfig(config); - + using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sql, command, configuration); @@ -308,7 +310,7 @@ public IEnumerable ExecuteRawSql(string sql, Func map, return items; } - + /// /// Gets the transaction /// @@ -334,7 +336,7 @@ public void UseTransaction(DbTransaction transaction) { Context.Database.UseTransaction(transaction); } - + /// /// Uses the transaction using the specified transaction /// @@ -344,10 +346,10 @@ public Task UseTransactionAsync(DbTransaction transaction, CancellationToken can { return Context.Database.UseTransactionAsync(transaction, cancellationToken); } - + /// - /// Gets the query function using the specified name. Parameter values are emitted as - /// positional placeholders ({0}, {1}, …) so the values travel as + /// Gets the query function using the specified name. Parameter values are emitted as positional + /// placeholders ({0}, {1}, …) so the values travel as /// s through FromSqlRaw and are never /// interpolated into the SQL text. /// @@ -360,15 +362,15 @@ internal static string GetQueryFunction(string name, SqlConfiguration config) } /// - /// Gets the query procedure using the specified name. PostgreSQL invokes stored procedures with - /// CALL; parameter values are emitted as named placeholders matching the - /// s created by , so the values are never - /// interpolated into the SQL text. + /// Builds the stored-procedure invocation. The default is the ANSI CALL name(args) form; + /// SQL Server overrides this with EXEC name args. Parameter values are emitted as named + /// placeholders matching the s created by , so + /// the values are never interpolated into the SQL text. /// - /// The name + /// The procedure name. /// The configuration. - /// The string - internal static string GetQueryProcedure(string name, SqlConfiguration config) + /// The SQL text. + internal virtual string BuildProcedureSql(string name, SqlConfiguration config) { return $"CALL {name}({GetNamedPlaceholders(config.Parameters.ToArray())} )"; } @@ -441,6 +443,7 @@ private static string ParseSql(string sql, SqlConfiguration config) { return !string.IsNullOrEmpty(config.Tag) ? GetQueryWithTag(sql, config.Tag) : sql; } + /// /// Sets the command using the specified command timeout /// @@ -489,15 +492,15 @@ private static string GetQueryWithTag(string query, string tag) queryBuilder.Append(query); return queryBuilder.ToString(); } - - private static SqlConfiguration GetConfig(Action config = null) + + private static DefaultSqlConfiguration GetConfig(Action config = null) { var configuration = new DefaultSqlConfiguration(); config?.Invoke(configuration); return configuration; } - + /// /// Disposes this instance /// @@ -506,7 +509,7 @@ public void Dispose() Dispose(true); GC.SuppressFinalize(this); } - + /// /// Disposes the disposing /// diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs new file mode 100644 index 0000000..1038997 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Options; +using eQuantic.Core.Data.Repository.Sql; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository; + +/// +/// The shared non-generic relational unit of work. Providers derive their own thin +/// UnitOfWork<TDbContext> from . +/// +public abstract class RelationalUnitOfWork : RelationalSqlExecutor +{ + protected static int IsMigrating; + + protected RelationalUnitOfWork(DbContext context) : base(context) + { + } + + internal DbContext GetDbContext() => Context; +} + +public abstract class RelationalUnitOfWork : RelationalUnitOfWork, ISqlUnitOfWork + where TDbContext : DbContext +{ + private readonly TDbContext _context; + private readonly IServiceProvider _serviceProvider; + + protected RelationalUnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(context) + { + _serviceProvider = serviceProvider; + _context = context; + } + + public int Commit() + { + return _context.SaveChanges(); + } + + public int CommitAndRefreshChanges() + { + var changes = 0; + var saveFailed = false; + + do + { + try + { + changes = _context.SaveChanges(); + + saveFailed = false; + } + catch (DbUpdateConcurrencyException ex) + { + saveFailed = true; + + ex.Entries.ToList() + .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); + } + } while (saveFailed); + + return changes; + } + + public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) + { + var changes = 0; + var saveFailed = false; + + do + { + try + { + changes = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + saveFailed = false; + } + catch (DbUpdateConcurrencyException ex) + { + saveFailed = true; + + ex.Entries.ToList() + .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); + } + } while (saveFailed); + + return changes; + } + + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public int Commit(Action options) + { + return Commit(); + } + + public int CommitAndRefreshChanges(Action options) + { + return CommitAndRefreshChanges(); + } + + public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) + { + return CommitAndRefreshChangesAsync(cancellationToken); + } + + public Task CommitAsync(Action options, CancellationToken cancellationToken = default) + { + return CommitAsync(cancellationToken); + } + + Data.Repository.ISet IQueryableUnitOfWork.CreateSet() => InternalCreateSet(); + + public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() + { + ((RelationalSet)InternalCreateSet()).ApplyCurrentValues(original, current); + } + + public void Attach(TEntity item) where TEntity : class, IEntity, new() + { + ((RelationalSet)InternalCreateSet()).Attach(item); + } + + public IEnumerable GetPendingMigrations() + { + return _context.Database.GetPendingMigrations(); + } + + public void LoadProperty(TEntity item, Expression> selector) + where TEntity : class, IEntity, new() + where TComplexProperty : class + { + ((RelationalSet)InternalCreateSet()).LoadProperty(item, selector); + } + + public void LoadProperty(TEntity item, string propertyName) + where TEntity : class, IEntity, new() + { + ((RelationalSet)InternalCreateSet()).LoadProperty(item, propertyName); + } + + public Task LoadPropertyAsync(TEntity item, Expression> selector, CancellationToken cancellationToken = default) + where TEntity : class, IEntity, new() + where TComplexProperty : class + { + return ((RelationalSet)InternalCreateSet()).LoadPropertyAsync(item, selector, cancellationToken); + } + + public Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) + where TEntity : class, IEntity, new() + { + return ((RelationalSet)InternalCreateSet()).LoadPropertyAsync(item, propertyName, cancellationToken); + } + + public void LoadCollection(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) where TEntity : class where TElement : class + { + if (filter != null) + { + _context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); + } + else + { + _context.Entry(item).Collection(navigationProperty).Load(); + } + } + + public async Task LoadCollectionAsync(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) where TEntity : class where TElement : class + { + if (filter != null) + { + await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); + } + else + { + await _context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); + } + } + + public void Reload(TEntity item) where TEntity : class + { + var entry = _context.Entry(item); + entry.CurrentValues.SetValues(entry.OriginalValues); + entry.Reload(); + } + + public void RollbackChanges() + { + // set all entities in change tracker + // as 'unchanged state' + _context?.ChangeTracker.Entries() + .ToList() + .ForEach(entry => entry.State = EntityState.Unchanged); + } + + public void SetModified(TEntity item) where TEntity : class + { + //this operation also attach item in object state manager + _context.Entry(item).State = EntityState.Modified; + } + + public void UpdateDatabase() + { + if (0 != Interlocked.Exchange(ref IsMigrating, 1)) + { + return; + } + + try + { + _context.Database.Migrate(); + } + finally + { + Interlocked.Exchange(ref IsMigrating, 0); + } + } + + public virtual Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => + InternalCreateSet(); + + public virtual SaveOptions GetSaveOptions() + { + return new SaveOptions(); + } + + public virtual IRepository GetRepository() + where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork + { + var repo = _serviceProvider.GetRequiredService>(); + return repo; + } + + public IAsyncRepository GetAsyncRepository() + where TEntity : class, IEntity, new() + where TUnitOfWork : IUnitOfWork + { + return _serviceProvider.GetRequiredService>(); + } + + public IQueryableRepository GetQueryableRepository() + where TEntity : class, IEntity, new() + where TUnitOfWork : IQueryableUnitOfWork + { + return _serviceProvider.GetRequiredService>(); + } + + public IAsyncQueryableRepository GetAsyncQueryableRepository() + where TEntity : class, IEntity, new() + where TUnitOfWork : IQueryableUnitOfWork + { + return _serviceProvider.GetRequiredService>(); + } + + private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => + new RelationalSet(_context); +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj new file mode 100644 index 0000000..34f03d8 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj @@ -0,0 +1,83 @@ + + + + + Shared relational implementation for eQuantic Core Data Entity Framework providers + eQuantic.Core.Data.EntityFramework.Relational + 1.0.0.0 + eQuantic Systems + net6.0;net7.0;net8.0;net9.0;net10.0 + eQuantic.Core.Data.EntityFramework.Relational + eQuantic.Core.Data.EntityFramework.Relational + eQuantic;Core;Data;Library;Repository;Pattern;SQL;Relational + Shared relational base used by the SqlServer, PostgreSql and MySql providers + + https://github.com/eQuantic/core-data-entityframework + ../../artifacts/ + false + false + false + True + https://github.com/eQuantic/core-data-entityframework + Git + LICENSE + README.md + Copyright © 2016 + 1.0.0.0 + 1.0.0.0 + Icon.png + latest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_Parameter1>eQuantic.Core.Data.EntityFramework.SqlServer + + + <_Parameter1>eQuantic.Core.Data.EntityFramework.PostgreSql + + + <_Parameter1>eQuantic.Core.Data.EntityFramework.MySql + + + <_Parameter1>eQuantic.Core.Data.EntityFramework.SqlServer.Tests + + + + + + + diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/ExpressionConverter.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/ExpressionConverter.cs deleted file mode 100644 index 5ce05bf..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/ExpressionConverter.cs +++ /dev/null @@ -1,176 +0,0 @@ -#if NET7_0_OR_GREATER && !NET10_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Query; - -namespace eQuantic.Core.Data.EntityFramework.SqlServer; - -internal class ExpressionConverter -{ - public static Expression, SetPropertyCalls>> ConvertExpression( - Expression> updateExpression) - { - ArgumentNullException.ThrowIfNull(updateExpression); - - var parameter = Expression.Parameter(typeof(SetPropertyCalls), "e"); - var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); - var block = methodCalls.Last(); - - return Expression.Lambda, SetPropertyCalls>>(block, parameter); - } - - private class ExpressionRewriter(ParameterExpression parameter) : ExpressionVisitor - { - private readonly List _setPropertyCalls = []; - - public IEnumerable Rewrite(Expression body) - { - if (body is not MemberInitExpression memberInit) - throw new NotSupportedException("Only MemberInit expressions are supported."); - - MethodCallExpression currentExpression = null; - foreach (var binding in memberInit.Bindings.OfType()) - { - currentExpression = RewriteBinding(binding, currentExpression); - } - - return _setPropertyCalls; - } - - private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCallExpression callExpression = null) - { - var propertyInfo = (PropertyInfo)binding.Member; - var propertyType = propertyInfo.PropertyType; - var entityParameter = Expression.Parameter(typeof(T), "entity"); - var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); - var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - var setPropertyMethod = typeof(SetPropertyCalls) - .GetMethods() - .Where(m => m.Name == nameof(SetPropertyCalls.SetProperty) && m.IsGenericMethod) - .Single(m => - { - var parameters = m.GetParameters(); - var genericArgs = m.GetGenericArguments(); - return genericArgs.Length == 1 && - parameters.Length == 2 && - IsGenericFunc(parameters[0].ParameterType) && - parameters[1].ParameterType == genericArgs[0]; - }) - .MakeGenericMethod(propertyType); - - var setPropertyCall = Expression.Call( - callExpression != null ? callExpression.Reduce() : parameter, - setPropertyMethod, - propertyLambda, - binding.Expression - ); - - _setPropertyCalls.Add(setPropertyCall); - - return setPropertyCall; - } - - private static bool IsGenericFunc(Type type) - { - return type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(Func<,>); - } - } -} -#endif -#if NET10_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Query; - -namespace eQuantic.Core.Data.EntityFramework.SqlServer; - -internal class ExpressionConverter -{ - public static Action> ConvertExpression( - Expression> updateExpression) - { - ArgumentNullException.ThrowIfNull(updateExpression); - - var parameter = Expression.Parameter(typeof(UpdateSettersBuilder), "e"); - var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); - var lastCall = methodCalls.Last(); - var body = lastCall.Type == typeof(void) - ? lastCall - : Expression.Block(typeof(void), lastCall); - return Expression.Lambda>>(body, parameter).Compile(); - } - - private class ExpressionRewriter(ParameterExpression parameter) : ExpressionVisitor - { - private readonly List _setPropertyCalls = []; - - public IEnumerable Rewrite(Expression body) - { - if (body is not MemberInitExpression memberInit) - throw new NotSupportedException("Only MemberInit expressions are supported."); - - MethodCallExpression currentExpression = null; - foreach (var binding in memberInit.Bindings.OfType()) - { - currentExpression = RewriteBinding(binding, currentExpression); - } - - return _setPropertyCalls; - } - - private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCallExpression callExpression = null) - { - var propertyInfo = (PropertyInfo)binding.Member; - var propertyType = propertyInfo.PropertyType; - - var entityParameter = Expression.Parameter(typeof(T), "entity"); - var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); - var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - - var setPropertyMethod = typeof(UpdateSettersBuilder) - .GetMethods() - .Where(m => m.Name == nameof(UpdateSettersBuilder.SetProperty) && m.IsGenericMethod) - .Single(m => - { - var parameters = m.GetParameters(); - var genericArgs = m.GetGenericArguments(); - return genericArgs.Length == 1 && - parameters.Length == 2 && - IsExpressionFunc(parameters[0].ParameterType) && - parameters[1].ParameterType == genericArgs[0]; - }) - .MakeGenericMethod(propertyType); - - var setPropertyCall = Expression.Call( - callExpression != null ? callExpression : parameter, - setPropertyMethod, - propertyLambda, - binding.Expression - ); - - _setPropertyCalls.Add(setPropertyCall); - - return setPropertyCall; - } - - private static bool IsExpressionFunc(Type type) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Expression<>)) - { - var delegateType = type.GetGenericArguments()[0]; - - return delegateType.IsGenericType && - delegateType.GetGenericTypeDefinition() == typeof(Func<,>); - } - return false; - } - } -} -#endif \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs index b5eaf8a..0f58d0c 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs @@ -1,287 +1,16 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; -#if NET6_0 || NETSTANDARD2_1 -using Z.EntityFramework.Plus; -#endif namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository; -public class Set : SetBase where TEntity : class, IEntity, new() +/// +/// SQL Server entity set. The implementation lives in ; this +/// type is preserved for source compatibility. +/// +public class Set : RelationalSet where TEntity : class, IEntity, new() { public Set(DbContext context) : base(context) { } - - public override long DeleteMany(Expression> filter) - { -#if NET6_0 || NETSTANDARD2_1 - return InternalDbSet.Where(filter).Delete(); -#else - return InternalDbSet.Where(filter).ExecuteDelete(); -#endif - } - - public override async Task DeleteManyAsync(Expression> filter, - CancellationToken cancellationToken = default) - { -#if NET6_0 || NETSTANDARD2_1 - return await InternalDbSet.Where(filter).DeleteAsync(cancellationToken).ConfigureAwait(false); -#else - return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); -#endif - } - - public void LoadCollection(TChildEntity item, - Expression>> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Collection(selector).Load(); - } - - public void LoadCollection(TChildEntity item, string propertyName) - where TChildEntity : class - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - - public async Task LoadCollectionAsync(TChildEntity item, - Expression>> selector) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Collection(selector).LoadAsync().ConfigureAwait(false); - } - - public async Task LoadCollectionAsync(TChildEntity item, string propertyName) - where TChildEntity : class - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync().ConfigureAwait(false); - } - - public void LoadProperties(TEntity entity, params string[] properties) - { - if (properties is not { Length: > 0 }) - { - return; - } - - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - LoadProperty(entity, property); - } - else - { - LoadCascade(props, entity); - } - } - } - - public async Task LoadPropertiesAsync(TEntity entity, params string[] properties) - { - if (properties is { Length: > 0 }) - { - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - await LoadPropertyAsync(entity, property).ConfigureAwait(false); - } - else - { - await LoadCascadeAsync(props, entity).ConfigureAwait(false); - } - } - } - } - - public void LoadProperty(TChildEntity item, - Expression> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Reference(selector).Load(); - } - - public void LoadProperty(TChildEntity item, string propertyName) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - else - { - DbContext.Entry(item).Reference(propertyName).Load(); - } - } - - public async Task LoadPropertyAsync(TChildEntity item, - Expression> selector, CancellationToken cancellationToken = default) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task LoadPropertyAsync(TChildEntity item, string propertyName, - CancellationToken cancellationToken = default) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); - } - else - { - await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); - } - } - - public override long UpdateMany(Expression> filter, - Expression> updateExpression) - { -#if NET6_0 || NETSTANDARD2_1 - return InternalDbSet.Where(filter).Update(updateExpression); -#else - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return InternalDbSet.Where(filter).ExecuteUpdate(convertedExpression); -#endif - } - - public override async Task UpdateManyAsync(Expression> filter, - Expression> updateExpression, CancellationToken cancellationToken = default) - { -#if NET6_0 || NETSTANDARD2_1 - return await InternalDbSet.Where(filter).UpdateAsync(updateExpression, cancellationToken).ConfigureAwait(false); -#else - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken).ConfigureAwait(false); -#endif - } - - private void LoadCascade(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - LoadProperty(obj, props[index]); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - LoadCascade(props, nextObj, index + 1); - } - } - - private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - await LoadPropertyAsync(obj, props[index]).ConfigureAwait(false); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - await LoadCascadeAsync(props, nextObj, index + 1).ConfigureAwait(false); - } - } - - internal Expression> GetExpression(TKey id) - { - return DbContext.GetFindByKeyExpression(id); - } - - public override IQueryable GetQueryable(Action configuration, - Func, IQueryable> internalQueryAction) - { - if (configuration == null) - { - return internalQueryAction.Invoke(this); - } - - var config = GetConfig(configuration); - var queryableConfig = config as QueryableConfiguration; - - var query = string.IsNullOrEmpty(queryableConfig?.SqlRaw) ? this : InternalDbSet.FromSqlRaw(queryableConfig.SqlRaw); - - if (config.HasNoTracking) - { - query = query.AsNoTracking(); - } - - if (config.Properties?.Any() == true) - { - query = query.IncludeMany(config.Properties.ToArray()); - } - - if (queryableConfig?.IgnoreQueryFilters == true) - { - query = query.IgnoreQueryFilters(); - } - - if (!string.IsNullOrEmpty(config.Tag)) - { - query = query.TagWith(config.Tag); - } - - if (queryableConfig != null) - { - query = queryableConfig.BeforeCustomization.Invoke(query); - } - - query = internalQueryAction.Invoke(query); - - if (config.SortingColumns.Any()) - { - query = query.OrderBy(config.SortingColumns.ToArray()); - } - - if (queryableConfig != null) - { - query = queryableConfig.AfterCustomization.Invoke(query); - } - - return query; - } } diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs deleted file mode 100644 index eba162f..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs +++ /dev/null @@ -1,528 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.SqlServer.Repository.Extensions; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Sql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; - -namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository; - -/// -/// The sql executor class -/// -/// -/// -/// -[ExcludeFromCodeCoverage] -public abstract class SqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable -{ - /// - /// The context - /// - protected readonly DbContext Context; - - /// - /// The disposed - /// - protected bool Disposed; - - /// - /// The transaction - /// - protected IDbContextTransaction Transaction; - - /// - /// Initializes a new instance of the class - /// - /// The context - protected SqlExecutor(DbContext context) - { - Context = context; - } - - /// - /// Begins the transaction using the specified cancellation token - /// - /// The cancellation token - public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) - { - Transaction?.Dispose(); - Transaction = await Context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - } - - /// - /// Commits the transaction using the specified cancellation token - /// - /// The cancellation token - public virtual async Task CommitTransactionAsync(CancellationToken cancellationToken = default) - { - if (Transaction != null) - { - await Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - } - } - - /// - /// Rollbacks the transaction using the specified cancellation token - /// - /// The cancellation token - public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) - { - if (Transaction != null) - { - await Transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); - } - } - - /// - public async Task> ExecuteRawSqlAsync(string sql, Func map, - Action config = null, CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - - await using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sql, command, configuration); - - await Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); - await using var result = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); - - var items = new List(); - while (await result.ReadAsync(cancellationToken).ConfigureAwait(false)) - { - items.Add(map(result)); - } - - return items; - } - - /// - /// Executes the non query using the specified sql command - /// - /// The sql command - /// The configuration. - /// The int - public int ExecuteNonQuery(string sqlCommand, Action config = null) - { - var configuration = GetConfig(config); - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - Context.Database.OpenConnection(); - return command.ExecuteNonQuery(); - } - - /// - /// Executes the function using the specified name - /// - /// The result - /// The name - /// The configuration. - /// The result - public TResult ExecuteFunction(string name, Action config = null) where TResult : class - { - var configuration = GetConfig(config); - var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set() - .FromSqlRaw(sql, GetParameterValues(configuration)) - .FirstOrDefault(); - } - - /// - /// Executes the command using the specified command timeout - /// - /// The sql command - /// The configuration. - /// The int - public int ExecuteCommand(string sqlCommand, Action config = null) - { - var configuration = GetConfig(config); - - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - Context.Database.OpenConnection(); - var result = command.ExecuteScalar(); - - return result == DBNull.Value ? 0 : Convert.ToInt32(result); - } - - /// - /// Executes the command using the specified command timeout - /// - /// The sql command - /// - /// The cancellation token - /// A task containing the int - public async Task ExecuteCommandAsync(string sqlCommand, - Action config = null, CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - - await using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - await Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); - var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); - - return result == DBNull.Value ? 0 : Convert.ToInt32(result); - } - - /// - /// Executes the function using the specified name - /// - /// The result - /// The name - /// - /// The cancellation token - /// A task containing the result - public Task ExecuteFunctionAsync(string name, - Action config = null, - CancellationToken cancellationToken = default) where TResult : class - { - var configuration = GetConfig(config); - var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)) - .FirstOrDefaultAsync(cancellationToken); - } - - /// - /// Executes the procedure using the specified name - /// - /// The name - /// The configuration - /// The int - public int ExecuteProcedure(string name, Action config = null) - { - var configuration = GetConfig(config); - return ExecuteCommand(GetQueryProcedure(name, configuration) + ";", config); - } - - /// - /// Executes the query using the specified sql query - /// - /// The entity - /// The sql query - /// The configuration. - /// An enumerable of t entity - public IEnumerable ExecuteQuery(string sqlQuery, Action config = null) where TEntity : class - { - var configuration = GetConfig(config); - var sql = ParseSql(sqlQuery, configuration); - return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)); - } - - /// - /// Executes the transaction using the specified operation - /// - /// The operation - /// - public void ExecuteTransaction(Action operation) - { - if (operation == null) - { - throw new ArgumentNullException(nameof(operation)); - } - - var strategy = Context.Database.CreateExecutionStrategy(); - - strategy.Execute(() => { operation.Invoke((ISqlUnitOfWork)this); }); - } - - /// - /// Executes the procedure using the specified name - /// - /// The name - /// - /// The cancellation token - /// A task containing the int - public Task ExecuteProcedureAsync(string name, Action config = null, - CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - return ExecuteCommandAsync(GetQueryProcedure(name, configuration) + ";", config, cancellationToken); - } - - /// - public Task ExecuteTransactionAsync(Func operation, - CancellationToken cancellationToken = default) - { - if (operation == null) - { - throw new ArgumentNullException(nameof(operation)); - } - - var strategy = Context.Database.CreateExecutionStrategy(); - - return strategy.ExecuteAsync(() => operation.Invoke((ISqlUnitOfWork)this)); - } - - /// - /// Begins the transaction - /// - public void BeginTransaction() - { - Transaction?.Dispose(); - Transaction = Context.Database.BeginTransaction(); - } - - /// - /// Commits the transaction - /// - public virtual void CommitTransaction() - { - Transaction?.Commit(); - } - - /// - /// Executes the raw sql using the specified sql - /// - /// The - /// The sql - /// The map - /// The configuration. - /// The items - public IEnumerable ExecuteRawSql(string sql, Func map, - Action config = null) - { - var configuration = GetConfig(config); - - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sql, command, configuration); - - Context.Database.OpenConnection(); - using var result = command.ExecuteReader(); - - var items = new List(); - while (result.Read()) - { - items.Add(map(result)); - } - - return items; - } - - /// - /// Gets the transaction - /// - /// The db transaction - public DbTransaction GetTransaction() - { - return Transaction?.GetDbTransaction(); - } - - /// - /// Rollbacks the transaction - /// - public void RollbackTransaction() - { - Transaction?.Rollback(); - } - - /// - /// Uses the transaction using the specified transaction - /// - /// The transaction - public void UseTransaction(DbTransaction transaction) - { - Context.Database.UseTransaction(transaction); - } - - /// - /// Uses the transaction using the specified transaction - /// - /// The transaction - /// The cancellation token - public Task UseTransactionAsync(DbTransaction transaction, CancellationToken cancellationToken = default) - { - return Context.Database.UseTransactionAsync(transaction, cancellationToken); - } - - /// - /// Gets the query function using the specified name. Parameter values are emitted as - /// positional placeholders ({0}, {1}, …) so the values travel as - /// s through FromSqlRaw and are never - /// interpolated into the SQL text. - /// - /// The name - /// The configuration. - /// The string - internal static string GetQueryFunction(string name, SqlConfiguration config) - { - return $"SELECT {name}({GetPositionalPlaceholders(config.Parameters.Count)} )"; - } - - /// - /// Gets the query procedure using the specified name. Parameter values are emitted as named - /// placeholders matching the s created by , - /// so the values are never interpolated into the SQL text. - /// - /// The name - /// The configuration. - /// The string - internal static string GetQueryProcedure(string name, SqlConfiguration config) - { - return $"EXEC {name}{GetNamedPlaceholders(config.Parameters.ToArray())}"; - } - - /// - /// Builds a comma-separated list of positional placeholders ({0}, {1}, …) for the - /// given parameter count. Used by FromSqlRaw, which substitutes each placeholder with a - /// parameter reference. - /// - /// The number of parameters. - /// The placeholder string. - internal static string GetPositionalPlaceholders(int count) - { - var cmdBuilder = new StringBuilder(); - for (var i = 0; i < count; i++) - { - if (i > 0) - { - cmdBuilder.Append(','); - } - - cmdBuilder.Append(" {").Append(i).Append('}'); - } - - return cmdBuilder.ToString(); - } - - /// - /// Builds a comma-separated list of named placeholders (@Param0, @Name, …) using - /// the same naming convention as , so the placeholders bind to the - /// parameters added to the command. - /// - /// The parameters - /// The placeholder string. - internal static string GetNamedPlaceholders(params ParamValue[] parameters) - { - var cmdBuilder = new StringBuilder(); - if (parameters is not { Length: > 0 }) - { - return cmdBuilder.ToString(); - } - - for (var i = 0; i < parameters.Length; i++) - { - if (i > 0) - { - cmdBuilder.Append(','); - } - - var parameterName = string.IsNullOrEmpty(parameters[i].Name) ? $"Param{i}" : parameters[i].Name; - cmdBuilder.Append(" @").Append(parameterName); - } - - return cmdBuilder.ToString(); - } - - /// - /// Gets the ordered parameter values used to feed FromSqlRaw's positional placeholders. - /// - /// The configuration. - /// The parameter values, in the same order as the emitted placeholders. - internal static object[] GetParameterValues(SqlConfiguration config) - { - return config.Parameters == null - ? Array.Empty() - : config.Parameters.Select(p => p.Value).ToArray(); - } - - private static string ParseSql(string sql, SqlConfiguration config) - { - return !string.IsNullOrEmpty(config.Tag) ? GetQueryWithTag(sql, config.Tag) : sql; - } - /// - /// Sets the command using the specified command timeout - /// - /// The sql command - /// The command - /// The configuration. - private void SetCommand(string sqlCommand, DbCommand command, SqlConfiguration config) - { - if (Transaction != null) - { - command.Transaction = Transaction.GetDbTransaction(); - } - - command.CommandText = ParseSql(sqlCommand, config); - command.CommandType = CommandType.Text; - command.CommandTimeout = config.GetCommandTimeout(Context); - - if (config.Parameters == null) - { - return; - } - - var i = 0; - foreach (var t in config.Parameters) - { - var parameterName = string.IsNullOrEmpty(t.Name) ? $"Param{i}" : t.Name; - var parameter = command.CreateParameter(); - parameter.ParameterName = parameterName; - parameter.Value = t.Value; - command.Parameters.Add(parameter); - i++; - } - } - - /// - /// Gets the query with tag using the specified query - /// - /// The query - /// The tag - /// The string - private static string GetQueryWithTag(string query, string tag) - { - var queryBuilder = new StringBuilder(); - queryBuilder.AppendLine($"--{tag}"); - queryBuilder.AppendLine(); - queryBuilder.Append(query); - return queryBuilder.ToString(); - } - - private static SqlConfiguration GetConfig(Action config = null) - { - var configuration = new DefaultSqlConfiguration(); - config?.Invoke(configuration); - - return configuration; - } - - /// - /// Disposes this instance - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes the disposing - /// - /// The disposing - protected virtual void Dispose(bool disposing) - { - if (Disposed) - { - return; - } - - if (disposing) - { - Transaction?.Dispose(); - Context?.Dispose(); - } - - Disposed = true; - } -} diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs index 8dfefeb..88a70e7 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs @@ -1,266 +1,25 @@ -using System; -using System.Collections.Generic; +using System; using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Options; -using eQuantic.Core.Data.Repository.Sql; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; +using eQuantic.Core.Data.Repository.Config; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository; -public abstract class UnitOfWork : SqlExecutor -{ - protected static int IsMigrating; - - protected UnitOfWork(DbContext context) : base(context) - { - } - - internal DbContext GetDbContext() => Context; -} - -public abstract class UnitOfWork : UnitOfWork, ISqlUnitOfWork +/// +/// SQL Server unit of work. The implementation lives in +/// ; this type only supplies the SQL Server dialect +/// (stored procedures are invoked with EXEC rather than the ANSI CALL). +/// +public abstract class UnitOfWork : RelationalUnitOfWork where TDbContext : DbContext { - private readonly TDbContext _context; - private readonly IServiceProvider _serviceProvider; - - protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(context) - { - _serviceProvider = serviceProvider; - _context = context; - } - - public int Commit() - { - return _context.SaveChanges(); - } - - public int CommitAndRefreshChanges() - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = _context.SaveChanges(); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAsync(CancellationToken cancellationToken = default) - { - return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public int Commit(Action options) - { - return Commit(); - } - - public int CommitAndRefreshChanges(Action options) - { - return CommitAndRefreshChanges(); - } - - public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAndRefreshChangesAsync(cancellationToken); - } - - public Task CommitAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAsync(cancellationToken); - } - - Data.Repository.ISet IQueryableUnitOfWork.CreateSet() => InternalCreateSet(); - - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).ApplyCurrentValues(original, current); - } - - public void Attach(TEntity item) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).Attach(item); - } - - public IEnumerable GetPendingMigrations() - { - return _context.Database.GetPendingMigrations(); - } - - public void LoadProperty(TEntity item, Expression> selector) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - ((Set)InternalCreateSet()).LoadProperty(item, selector); - } - - public void LoadProperty(TEntity item, string propertyName) - where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).LoadProperty(item, propertyName); - } - - public Task LoadPropertyAsync(TEntity item, Expression> selector, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, selector, cancellationToken); - } - - public Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, propertyName, cancellationToken); - } - - public void LoadCollection(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - _context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); - } - else - { - _context.Entry(item).Collection(navigationProperty).Load(); - } - } - - public async Task LoadCollectionAsync(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); - } - else - { - await _context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); - } - } - - public void Reload(TEntity item) where TEntity : class - { - var entry = _context.Entry(item); - entry.CurrentValues.SetValues(entry.OriginalValues); - entry.Reload(); - } - - public void RollbackChanges() - { - // set all entities in change tracker - // as 'unchanged state' - _context?.ChangeTracker.Entries() - .ToList() - .ForEach(entry => entry.State = EntityState.Unchanged); - } - - public void SetModified(TEntity item) where TEntity : class - { - //this operation also attach item in object state manager - _context.Entry(item).State = EntityState.Modified; - } - - public void UpdateDatabase() - { - if (0 != Interlocked.Exchange(ref IsMigrating, 1)) - { - return; - } - - try - { - _context.Database.Migrate(); - } - finally - { - Interlocked.Exchange(ref IsMigrating, 0); - } - } - - public virtual Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => - InternalCreateSet(); - - public virtual SaveOptions GetSaveOptions() - { - return new SaveOptions(); - } - - public virtual IRepository GetRepository() - where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork - { - var repo = _serviceProvider.GetRequiredService>(); - return repo; - } - - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(serviceProvider, context) { - return _serviceProvider.GetRequiredService>(); } - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + internal override string BuildProcedureSql(string name, SqlConfiguration config) { - return _serviceProvider.GetRequiredService>(); + return $"EXEC {name}{GetNamedPlaceholders(config.Parameters.ToArray())}"; } - - private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => - new Set(_context); } diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs index ae79002..2950072 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs @@ -1,7 +1,7 @@ using System; using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using eQuantic.Core.Data.EntityFramework.Repository.Extensions; -using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; using eQuantic.Core.Data.Repository; using eQuantic.Linq.Specification; @@ -11,9 +11,9 @@ public class GetEntityByIdSpecification : Specification where TEntity : class, IEntity, new() { private readonly TKey _id; - private readonly UnitOfWork _unitOfWork; + private readonly RelationalUnitOfWork _unitOfWork; - public GetEntityByIdSpecification(TKey id, UnitOfWork unitOfWork) + public GetEntityByIdSpecification(TKey id, RelationalUnitOfWork unitOfWork) { _id = id; _unitOfWork = unitOfWork; @@ -22,4 +22,4 @@ public override Expression> SatisfiedBy() { return _unitOfWork.GetDbContext().GetFindByKeyExpression(_id); } -} \ No newline at end of file +} diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj index d3e4fbb..d0a0a11 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj @@ -65,5 +65,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj index c749e4c..ac1d53d 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj @@ -66,5 +66,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj index d522b0d..de1e210 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj @@ -65,5 +65,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj index 9b6aa64..e8378a6 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj @@ -65,5 +65,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj index 0bad350..ed16f09 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj @@ -65,5 +65,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj index 1e9ade2..eec1ef5 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj @@ -145,5 +145,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj index f9b7a38..5db132e 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj @@ -108,6 +108,9 @@ <_Parameter1>$(AssemblyName).Tests + + <_Parameter1>$(AssemblyName).Relational + <_Parameter1>$(AssemblyName).SqlServer diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ExpressionConverterTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ExpressionConverterTests.cs index 2f167dc..0143a25 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ExpressionConverterTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ExpressionConverterTests.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.Relational; using Microsoft.EntityFrameworkCore.Query; namespace eQuantic.Core.Data.EntityFramework.SqlServer.Tests; diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs index bf1520f..c06b580 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs @@ -1,22 +1,38 @@ +using System; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.Repository.Sql; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace eQuantic.Core.Data.EntityFramework.SqlServer.Tests; /// /// Guards the fix for the SQL-injection defect: parameter values must be emitted as placeholders, -/// never interpolated into the SQL text. +/// never interpolated into the SQL text. The implementation now lives in the shared +/// ; SQL Server supplies the EXEC dialect. /// public class SqlExecutorParameterizationTests { private const string Malicious = "'; DROP TABLE Users; --"; + private sealed class TestDbContext(DbContextOptions options) : DbContext(options); + + private static DefaultUnitOfWork NewSqlServerUnitOfWork() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase($"sqlexec-{Guid.NewGuid():N}") + .Options; + return new DefaultUnitOfWork(new ServiceCollection().BuildServiceProvider(), new TestDbContext(options)); + } + [Test] public void GetQueryFunction_EmitsPositionalPlaceholders_NotValues() { var config = new DefaultSqlConfiguration().WithParameters(Malicious, 42); - var sql = SqlExecutor.GetQueryFunction("dbo.GetUser", config); + var sql = RelationalSqlExecutor.GetQueryFunction("dbo.GetUser", config); Assert.That(sql, Does.Contain("{0}")); Assert.That(sql, Does.Contain("{1}")); @@ -25,11 +41,11 @@ public void GetQueryFunction_EmitsPositionalPlaceholders_NotValues() } [Test] - public void GetQueryProcedure_EmitsNamedPlaceholders_NotValues() + public void BuildProcedureSql_SqlServer_UsesExecWithNamedPlaceholders_NotValues() { var config = new DefaultSqlConfiguration().WithParameters(Malicious, 42); - var sql = SqlExecutor.GetQueryProcedure("dbo.DoWork", config); + var sql = NewSqlServerUnitOfWork().BuildProcedureSql("dbo.DoWork", config); Assert.That(sql, Does.StartWith("EXEC dbo.DoWork")); Assert.That(sql, Does.Contain("@Param0")); @@ -39,12 +55,12 @@ public void GetQueryProcedure_EmitsNamedPlaceholders_NotValues() } [Test] - public void GetQueryProcedure_UsesProvidedParameterNames() + public void BuildProcedureSql_UsesProvidedParameterNames() { var config = new DefaultSqlConfiguration() - .WithParameters(ParamValueFor("userId", 7), ParamValueFor("state", "active")); + .WithParameters(ParamValue.Create("userId", 7), ParamValue.Create("state", "active")); - var sql = SqlExecutor.GetQueryProcedure("dbo.DoWork", config); + var sql = NewSqlServerUnitOfWork().BuildProcedureSql("dbo.DoWork", config); Assert.That(sql, Does.Contain("@userId")); Assert.That(sql, Does.Contain("@state")); @@ -55,7 +71,7 @@ public void GetParameterValues_PreservesValues_ForPositionalBinding() { var config = new DefaultSqlConfiguration().WithParameters(Malicious, 42); - var values = SqlExecutor.GetParameterValues(config); + var values = RelationalSqlExecutor.GetParameterValues(config); Assert.That(values, Has.Length.EqualTo(2)); Assert.That(values, Does.Contain(Malicious)); @@ -63,13 +79,10 @@ public void GetParameterValues_PreservesValues_ForPositionalBinding() } [Test] - public void GetPositionalPlaceholders_EmptyForNoParameters() + public void GetQueryFunction_EmptyForNoParameters() { - var sql = SqlExecutor.GetQueryFunction("dbo.NoArgs", new DefaultSqlConfiguration()); + var sql = RelationalSqlExecutor.GetQueryFunction("dbo.NoArgs", new DefaultSqlConfiguration()); Assert.That(sql, Does.Not.Contain("{0}")); } - - private static eQuantic.Core.Data.Repository.Sql.ParamValue ParamValueFor(string name, object value) - => eQuantic.Core.Data.Repository.Sql.ParamValue.Create(name, value); } From 1c4b6320cc9897209444fd60f67425b9ab63a724 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 15:47:41 +0000 Subject: [PATCH 19/32] docs: translate improvement plan to English and record Phase 2 status --- docs/IMPROVEMENT_PLAN.md | 534 ++++++++++++++++++++------------------- 1 file changed, 279 insertions(+), 255 deletions(-) diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md index 028590e..9350575 100644 --- a/docs/IMPROVEMENT_PLAN.md +++ b/docs/IMPROVEMENT_PLAN.md @@ -1,307 +1,331 @@ -# Plano de Melhoria — eQuantic.Core.Data.EntityFramework - -> Análise profunda realizada em 2026-07-16 sobre este repositório (v4.4.2 / linhas 6.x–10.x publicadas) -> e sobre o repositório de contratos [`eQuantic/core-data`](https://github.com/eQuantic/core-data) (v4.3.2). -> Todos os achados citam `arquivo:linha` e foram verificados no código-fonte, não inferidos. - -## Sumário executivo - -O pacote funciona e está publicado no nuget.org há anos (106 versões do pacote principal, 57 do -contrato), mas acumulou dívida em cinco camadas. Em ordem de gravidade: - -1. **Segurança (crítico):** o `SqlExecutor` monta SQL interpolando valores sem escape — injeção de SQL - real em `ExecuteFunction`/`ExecuteProcedure` nos providers SqlServer, PostgreSql e MySql. Além disso, - o CI publica no nuget.org a cada push em qualquer branch. -2. **Correção (crítico/alto):** disposal duplo do `UnitOfWork`, provider MongoDb operando no banco - errado (grava/apaga em silêncio), `EXEC` (T-SQL) copiado para PostgreSQL/MySQL, parâmetros perdidos no - `FromSqlRaw`, `configuration` descartado em `All`/`Any`, registro de DI que quebra em runtime para o - MongoDb, e updates em massa que gravam valores errados sem erro. -3. **Packaging/versionamento (crítico para consumidores):** o mesmo `PackageId` é publicado em linhas de - versão paralelas (4.x multi-target + 6.x/7.x/8.x/9.x/10.x single-target). O NuGet trata tudo como uma - linha do tempo única: "latest" = 10.0.2 (net10-only), quebrando o restore de quem está em net6–net9 e - fazendo a linha 4.x (a mais completa) parecer abandonada. São 21 csproj mantidos à mão, e o esquema já - produziu bugs reais de grafo de dependência. -4. **Duplicação estrutural:** ~2.400 linhas são cópias idênticas entre providers — PostgreSql e MySql são - 100% cópias renomeadas do SqlServer. É a causa-raiz do bug `EXEC`→`CALL` (copiado sem adaptar). -5. **Contratos (`eQuantic.Core.Data`):** explosão combinatória de interfaces - (`IAsyncReadRepository` tem **100 membros**; só `SumAsync` são 30 overloads), - NRT desabilitado, e a paginação retorna `IEnumerable` sem total de registros. - -O plano abaixo está em **5 fases**. As Fases 0–2 **não quebram contrato** e podem sair na linha atual. -As Fases 3–4 definem a **v5.0.0 dos contratos** (breaking deliberado) e a estratégia de versionamento -no nuget.org. - -## Status de implementação (Fase 1 — concluída) - -Correções já aplicadas nesta branch, **com testes** (28 testes passando; todos os 5 pacotes compilando — -base/SqlServer/PostgreSql/MySql em net10, MongoDb em net8): - -| Achado | Correção | Commit | +# Improvement Plan — eQuantic.Core.Data.EntityFramework + +> Deep analysis performed on 2026-07-16 of this repository (v4.4.2 / published 6.x–10.x lines) and of the +> contracts repository [`eQuantic/core-data`](https://github.com/eQuantic/core-data) (v4.3.2). +> Every finding cites `file:line` and was verified against the source code, not inferred. + +## Executive summary + +The package works and has been published on nuget.org for years (106 versions of the main package, 57 of +the contract), but it accumulated debt across five layers. In order of severity: + +1. **Security (critical):** `SqlExecutor` builds SQL by interpolating values without escaping — a real SQL + injection in `ExecuteFunction`/`ExecuteProcedure` across the SqlServer, PostgreSql and MySql providers. + In addition, CI publishes to nuget.org on every push to any branch. +2. **Correctness (critical/high):** double-dispose of the `UnitOfWork`, the MongoDb provider operating + against the wrong database (silently writing/deleting), `EXEC` (T-SQL) copied to PostgreSQL/MySQL, + parameters lost in `FromSqlRaw`, `configuration` discarded in `All`/`Any`, a DI registration that throws + at runtime for MongoDb, and bulk updates that silently write the wrong values. +3. **Packaging/versioning (critical for consumers):** the same `PackageId` is published as parallel version + lines (4.x multi-target + 6.x/7.x/8.x/9.x/10.x single-target). NuGet treats it all as a single timeline: + "latest" = 10.0.2 (net10-only), breaking restore for anyone on net6–net9 and making the 4.x line (the + most complete) look abandoned. There are 21 hand-maintained csproj files, and the scheme already produced + real dependency-graph bugs. +4. **Structural duplication:** ~2,400 lines are identical copies between providers — PostgreSql and MySql + are 100% renamed copies of SqlServer. That is the root cause of the `EXEC`→`CALL` bug (copied without + adapting the dialect). +5. **Contracts (`eQuantic.Core.Data`):** combinatorial interface explosion + (`IAsyncReadRepository` has **100 members**; `SumAsync` alone is 30 overloads), + NRT disabled, and pagination returning `IEnumerable` with no total count. + +The plan below is organized into **5 phases**. Phases 0–2 are **non contract-breaking** and can ship on the +current line. Phases 3–4 define the **v5.0.0 of the contracts** (deliberately breaking) and the versioning +strategy on nuget.org. + +## Implementation status (Phase 1 — done) + +Fixes already applied on this branch, **with tests** (28 tests passing; all packages build — +base/Relational/SqlServer/PostgreSql/MySql on net10, MongoDb on net8): + +| Finding | Fix | Commit | |--------|----------|--------| -| **S1** injeção de SQL + **P4** + **P3** | `SqlExecutor` parametrizado (placeholders em vez de interpolar valores) nos 3 providers SQL; `EXEC`→`CALL` em PG/MySql; `.ToArray()` no `ExecuteQuery`. Testes provam que o valor malicioso não chega ao SQL. | `🔒 fix(sql)` | -| **C3** crash de DI | `ISqlUnitOfWork` só registrado quando a impl o implementa; removido registro duplicado de `IQueryableUnitOfWork`. | `🐞 fix(core)` | -| **C1** double-dispose | `_disposed` unificado (`protected`); UoW disposto exatamente uma vez. | `🐞 fix(core)` | -| **A1** config descartada | `All`/`Any` sync repassam `configuration`. | `🐞 fix(read)` | -| **A2** chave default | `Get`/`GetAsync` validam `id is null` em vez de `default(TKey)`. | `🐞 fix(read)` | -| **P1/P6** MongoDb | Usa o banco configurado no `DbContext` (não o nome da coleção); lança em vez de retornar `0` silencioso. | `🐞 fix(mongodb)` | -| **A4/M7** chave | Valor da chave parametrizado (closure) em vez de literal; metadados de PK cacheados; `EF.Property` para shadow keys; lança em chave composta parcial. | `⚡ fix(core)` | -| **M4** DI | `AddRepository` respeita o lifetime configurado, usa `TryAdd`, e tolera `ReflectionTypeLoadException`. | `🐞 fix(di)` | -| **M2** async | `ConfigureAwait(false)` em 91 awaits de biblioteca (base + 4 providers). | `⚡ perf(async)` | -| **P2** MongoDb | `UpdateDefinitionBuilder` rejeita (lança) updates que referenciam a entidade em vez de gravar constante silenciosamente. Novo projeto de testes do MongoDb. | `🐞 fix(mongodb)` | -| **C2** ownership ⚠️ | O repositório **não** dispõe mais o `UnitOfWork` injetado (o criador — container DI ou chamador — é dono do ciclo de vida). **Mudança comportamental.** | `🐞 fix(core)` | -| **A5** paginação | `GetPaged`/`GetPagedAsync` ordenam pela PK quando não há ordenação explícita (paginação determinística); ordenação do chamador é preservada. | `🐞 fix(read)` | - -Substituído o teste placebo (`Assert.Pass()`) por cobertura real: DI, disposal, parametrização de SQL, -queries, expressão de chave, paginação e `UpdateDefinitionBuilder` do MongoDb, via EF InMemory. - -⚠️ **C2 é a única mudança comportamental do lote.** Quem dependia de dispor o repositório para fechar um -contexto criado manualmente passa a precisar dispor o `UnitOfWork`/`DbContext` diretamente (o container -DI já faz isso). Documentado no commit. - -**Investigado e corrigido no diagnóstico:** o item **M8** do plano (remover `Where(_ => true)` em -`GetAllAsync`) estava **errado** — esse `Where` é load-bearing (o `SetBase` não implementa -`IAsyncEnumerable`; o `Where` o converte num `IQueryable` real do EF). Documentado no código + teste de -regressão; nada removido. - -**Fase 1 concluída.** - -Nota sobre o **P2**: a correção atual **rejeita** updates que referenciam a entidade (evita corrupção -silenciosa). Suportá-los de fato via `$inc`/pipeline updates fica para uma iteração futura do provider -MongoDb. - -## Status de implementação (Fase 0 — concluída, escopo reduzido) - -| Achado | Correção | +| **S1** SQL injection + **P4** + **P3** | Parameterized `SqlExecutor` (placeholders instead of interpolating values) across the 3 SQL providers; `EXEC`→`CALL` on PG/MySql; `.ToArray()` in `ExecuteQuery`. Tests prove the malicious value never reaches the SQL. | `fix(sql)` | +| **C3** DI crash | `ISqlUnitOfWork` registered only when the implementation provides it; removed the duplicate `IQueryableUnitOfWork` registration. | `fix(core)` | +| **C1** double-dispose | Unified `_disposed` flag (`protected`); the UoW is disposed exactly once. | `fix(core)` | +| **A1** configuration discarded | Sync `All`/`Any` forward `configuration`. | `fix(read)` | +| **A2** default key | `Get`/`GetAsync` validate `id is null` instead of `default(TKey)`. | `fix(read)` | +| **P1/P6** MongoDb | Uses the database configured on the `DbContext` (not the collection name); throws instead of returning a silent `0`. | `fix(mongodb)` | +| **A4/M7** key | Key value parameterized (closure) instead of a literal; PK metadata cached; `EF.Property` for shadow keys; throws on a partial composite key. | `fix(core)` | +| **M4** DI | `AddRepository` honours the configured lifetime, uses `TryAdd`, and tolerates `ReflectionTypeLoadException`. | `fix(di)` | +| **M2** async | `ConfigureAwait(false)` across 91 library awaits (base + 4 providers). | `perf(async)` | +| **P2** MongoDb | `UpdateDefinitionBuilder` rejects (throws on) update expressions that reference the entity instead of silently writing a constant. New MongoDb test project. | `fix(mongodb)` | +| **C2** ownership ⚠️ | The repository no longer disposes the injected `UnitOfWork` (the creator — DI container or caller — owns the lifetime). **Behavioural change.** | `fix(core)` | +| **A5** pagination | `GetPaged`/`GetPagedAsync` order by the primary key when no explicit ordering is given (deterministic pagination); the caller's ordering is preserved. | `fix(read)` | + +The placebo test (`Assert.Pass()`) was replaced with real coverage: DI, disposal, SQL parameterization, +queries, the key expression, pagination and the MongoDb `UpdateDefinitionBuilder`, via EF InMemory. + +⚠️ **C2 is the only behavioural change in this batch.** Anyone who relied on disposing the repository to +close a manually-created context must now dispose the `UnitOfWork`/`DbContext` directly (the DI container +already does this). Documented in the commit. + +**Investigated and corrected during diagnosis:** plan item **M8** (remove `Where(_ => true)` in +`GetAllAsync`) was **wrong** — that `Where` is load-bearing (`SetBase` does not implement +`IAsyncEnumerable`; the `Where` turns it into a real EF `IQueryable`). Documented in code + a regression +test; nothing removed. + +Note on **P2**: the current fix **rejects** update expressions that reference the entity (avoids silent +corruption). Actually supporting them via `$inc`/pipeline updates is left for a future MongoDb iteration. + +## Implementation status (Phase 0 — done, reduced scope) + +| Finding | Fix | |--------|----------| -| **PK7** MSBump morto | Removido `build/MSBump.props` (import circular), `build/MSBump.targets` (task inexistente) e `build/Directory.Build.targets` (fora da cadeia de ancestrais — nunca era importado). Nada os referenciava; confirmado por grep antes de apagar. | -| **PK2** bug de grafo | `MySql.Net10.csproj` referenciava o core **Net9** em vez do **Net10** — corrigido. | -| **Q1** publicação por acidente | CI dividido em **`ci.yml`** (build + test em todo push/PR, sem publicar nada) e **`release.yml`** (só dispara em tag `vX.Y.Z`, publica atrás de um GitHub Environment `nuget-release`). | -| **Q2** sem testes no CI | `ci.yml` roda `dotnet test` nos 3 projetos de teste (antes não rodava nenhum). | -| **Q3** pipeline datado | Actions atualizadas (`checkout@v4`, `setup-dotnet@v4`), `ubuntu-latest` no lugar de `windows-latest`, cache de NuGet, `-p:ContinuousIntegrationBuild=true`. | -| **Q4** SDK não fixado | `global.json` na raiz fixando `10.0.100` com `rollForward: latestFeature`. | - -**Decisão de design (build por matriz, não por `dotnet build sln`):** tentei rodar -`dotnet build eQuantic.Core.Data.EntityFramework.sln` localmente para simplificar os 23 steps de build — -e reproduzi exatamente o problema do achado **PK4**: como vários `.csproj` de um mesmo pacote -(`eQuantic.Core.Data.EntityFramework.csproj`, `.Net6.csproj`, `.Net7.csproj`, …) compartilham a mesma -pasta sem `BaseIntermediateOutputPath` próprio, o build paralelo da solution corrompeu o -`project.assets.json` de uns com os outros e um `IOException` de arquivo em uso em outro. Os dois -workflows novos mantêm os 23 builds **individuais** (como o workflow antigo já fazia), mas cada um roda -numa **matrix job** — ou seja, em runner/checkout isolado — o que elimina o compartilhamento de -`obj`/`bin` sem precisar resolver a decisão de versionamento (Parte IV) primeiro. - -⚠️ **Duas coisas que só um mantenedor com acesso ao GitHub consegue terminar:** -1. **Nada publica automaticamente até existir uma tag.** Antes, qualquer push em qualquer branch tentava - publicar (mitigado só por `--skip-duplicate`). Agora é preciso `git tag vX.Y.Z && git push origin - vX.Y.Z` para disparar o `release.yml`. Isso é intencional (achado Q1), mas muda o fluxo de trabalho. -2. **O `environment: nuget-release` referenciado no `release.yml` não tem proteção nenhuma até ser - configurado.** O GitHub cria o Environment automaticamente no primeiro uso, sem revisores obrigatórios - nem restrição de branch/tag — a ferramenta de PR desta sessão não tem permissão para configurar isso. - Em *Settings → Environments → nuget-release*, adicionar ao menos um revisor obrigatório para o gate - funcionar de verdade. - -**Deliberadamente fora do escopo desta Fase 0** (não fiz, porque dependem da decisão de versionamento -ainda em aberto — Parte IV): adoção de MinVer (mexeria em como a `` de cada um dos 23 csproj é -determinada) e qualquer mudança nos números de versão publicados. Fazer isso agora, antes de decidir entre -consolidar numa linha única (opção A) ou manter `PackageId`s separados por .NET (opção B), arriscaria -retrabalho. - -**Próximos passos** (fases maiores, arquiteturais/breaking — aguardam definição de abordagem): -- **Fase 2** — de-duplicação dos providers (~2.400 linhas idênticas → base com hooks de dialeto). -- **Fase 3/4** — v5.0.0 dos contratos (`eQuantic.Core.Data`) e consolidação das linhas de versão no - nuget.org (ver Partes III e IV) — inclui a decisão de versionamento que bloqueia o MinVer. +| **PK7** dead MSBump | Removed `build/MSBump.props` (circular self-import), `build/MSBump.targets` (missing task) and `build/Directory.Build.targets` (outside any ancestor chain — never imported). Nothing referenced them; verified by grep before deleting. | +| **PK2** graph bug | `MySql.Net10.csproj` referenced the **Net9** base instead of **Net10** — fixed. | +| **Q1** accidental publishing | CI split into **`ci.yml`** (build + test on every push/PR, publishing nothing) and **`release.yml`** (only on a `vX.Y.Z` tag, publishing behind a `nuget-release` GitHub Environment). | +| **Q2** no tests in CI | `ci.yml` runs `dotnet test` on the 3 test projects (previously none ran). | +| **Q3** dated pipeline | Actions updated (`checkout@v4`, `setup-dotnet@v4`), `ubuntu-latest` instead of `windows-latest`, NuGet cache, `-p:ContinuousIntegrationBuild=true`. | +| **Q4** unpinned SDK | `global.json` at the root pinning `10.0.100` with `rollForward: latestFeature`. | + +**Design decision (matrix build, not `dotnet build sln`):** I tried running +`dotnet build eQuantic.Core.Data.EntityFramework.sln` locally to simplify the 23 build steps — and +reproduced exactly the **PK4** finding: because several `.csproj` files of the same package +(`eQuantic.Core.Data.EntityFramework.csproj`, `.Net6.csproj`, `.Net7.csproj`, …) share the same folder +without their own `BaseIntermediateOutputPath`, the parallel solution build corrupted each other's +`project.assets.json` and hit a file-in-use `IOException`. The two new workflows keep the builds +**individual** (as the old workflow already did), but each one runs as a **matrix job** — i.e. an isolated +runner/checkout — which eliminates the shared `obj`/`bin` without having to settle the versioning decision +(Part IV) first. + +⚠️ **Two things only a maintainer with GitHub access can finish:** +1. **Nothing publishes automatically until a tag exists.** Before, any push to any branch attempted to + publish (mitigated only by `--skip-duplicate`). Now you need `git tag vX.Y.Z && git push origin vX.Y.Z` + to trigger `release.yml`. This is intentional (finding Q1) but changes the workflow. +2. **The `environment: nuget-release` referenced by `release.yml` has no protection until configured.** + GitHub auto-creates the Environment on first use with no required reviewers or branch/tag restriction — + this session's PR tool has no permission to configure it. In *Settings → Environments → nuget-release*, + add at least one required reviewer for the gate to actually work. + +**Deliberately out of scope for Phase 0** (not done, because it depends on the still-open versioning +decision — Part IV): adopting MinVer (would change how each of the 23 csproj `` values is +determined) and any change to the published version numbers. Doing that now, before choosing between +consolidating to a single line (option A) and keeping per-.NET `PackageId`s (option B), would risk rework. + +## Implementation status (Phase 2 — done) + +Extracted the shared relational implementation into a **single package**, +`eQuantic.Core.Data.EntityFramework.Relational`, referenced by the 3 SQL providers. + +| Finding | Fix | +|--------|----------| +| **Structural duplication** (§4) | `RelationalSqlExecutor`, `RelationalUnitOfWork`/`RelationalUnitOfWork`, `RelationalSet` and the internal `ExpressionConverter`/`SqlConfigurationExtensions` now live once in the shared package. Each provider keeps thin `Set` and `UnitOfWork` subclasses plus `DefaultUnitOfWork`, so the consumer-facing types stay in their namespaces. **~2,200 fewer lines of duplicated source.** | +| **P3 root cause** | The only genuine dialect difference — stored procedures use `EXEC` on SQL Server and the ANSI `CALL` elsewhere — is a single `BuildProcedureSql` virtual, overridden only by SQL Server. The copy-paste that let `EXEC`/`CALL` diverge is gone. | +| **PK3 (partial)** | MySql's per-framework variants are realigned to reference the multi-target base project (matching SqlServer/PostgreSql) so the shared multi-target project does not pull a second copy of the base assembly. | + +**Why a new package rather than the base package or shared source** (the plan originally proposed "base +package with dialect hooks", which the deeper analysis showed to be wrong): +- The base package must **not** depend on `Microsoft.EntityFrameworkCore.Relational` — the MongoDb provider + references the base and is not relational (it depends only on `Microsoft.EntityFrameworkCore` core + + `MongoDB.Driver`). Putting relational code in the base would add `Relational` to every MongoDb consumer. +- **Linked source** (``) does not work for public types shared across providers: the type + would be compiled into every provider assembly and collide if a consumer references two providers at once + (a scenario the library supports). +- Changing the types' **namespace** would break the public API — that belongs to the v5 work. + +A separate shared assembly is therefore the only clean option. It is multi-target only (net6–net10, one +version line) — no per-framework variants needed, because SqlServer/PostgreSql already reference the base +multi-target project everywhere, so the shared multi-target project composes with them with a single base +assembly (no duplicate). + +Note: the implementation-only public types `SqlExecutor`, the non-generic `UnitOfWork` and +`SqlConfigurationExtensions` move to the `Relational` namespace, and `GetEntityByIdSpecification` now takes +`RelationalUnitOfWork`. These are implementation types (not the consumer-facing `DefaultUnitOfWork` / +`UnitOfWork` / `Set`), but referencing them by name is a minor source break. + +**What a maintainer still owns:** the new `eQuantic.Core.Data.EntityFramework.Relational` package is a new +published package the SQL providers now depend on. Its version (`1.0.0`) and how it fits the versioning +scheme is part of the Part IV decision. + +**Next steps** (larger, architectural/breaking phases — awaiting a chosen approach): +- **Phase 3/4** — v5.0.0 of the contracts (`eQuantic.Core.Data`) and consolidation of the version lines on + nuget.org (see Parts III and IV) — this includes the versioning decision that also blocks MinVer. --- -## Parte I — Diagnóstico +## Part I — Diagnosis -### 1. Segurança +### 1. Security -| # | Sev. | Problema | Local | +| # | Sev. | Problem | Location | |---|------|----------|-------| -| S1 | 🔴 Crítico | **Injeção de SQL**: `GetQueryParameters` interpola valores com `string.Format(" '{0}'", value)` sem escapar aspas simples; `string`/`Guid`/`DateTime` entram crus no texto SQL, que vai para `FromSqlRaw`/`ExecuteSqlRaw` **sem `DbParameter`**. O `name` da função/procedure também é interpolado. Arquivo idêntico nos 3 providers SQL. | `SqlServer/Repository/SqlExecutor.cs:367,375-407` (idem PostgreSql e MySql) | -| S2 | 🟡 Médio | Chave NuGet exposta a qualquer push: workflow publica com `secrets.nuget_key` em push de **qualquer branch**, sem environment protegido nem gate de tag/release. | `.github/workflows/dotnetcore.yml:3,66-67` | +| S1 | 🔴 Critical | **SQL injection**: `GetQueryParameters` interpolates values with `string.Format(" '{0}'", value)` without escaping single quotes; `string`/`Guid`/`DateTime` go raw into the SQL text, which reaches `FromSqlRaw`/`ExecuteSqlRaw` **with no `DbParameter`**. The function/procedure `name` is interpolated too. Identical file across the 3 SQL providers. | `SqlServer/Repository/SqlExecutor.cs:367,375-407` (same in PostgreSql and MySql) | +| S2 | 🟡 Medium | NuGet key exposed to any push: the workflow publishes with `secrets.nuget_key` on a push to **any branch**, with no protected environment and no tag/release gate. | `.github/workflows/dotnetcore.yml:3,66-67` | -**Correção do S1:** gerar placeholders (`@p0`/`$1`/`?`) e passar `DbParameter`s reais — a infraestrutura já -existe no próprio arquivo (`SetCommand`, `SqlExecutor.cs:419-445`) e é simplesmente ignorada nesses métodos. +**S1 fix:** emit placeholders (`@p0`/`$1`/`?`) and pass real `DbParameter`s — the infrastructure already +exists in the same file (`SetCommand`, `SqlExecutor.cs:419-445`) and is simply ignored by those methods. -### 2. Bugs de correção — Core (`eQuantic.Core.Data.EntityFramework`) +### 2. Correctness bugs — Core (`eQuantic.Core.Data.EntityFramework`) -| # | Sev. | Problema | Local | +| # | Sev. | Problem | Location | |---|------|----------|-------| -| C1 | 🔴 | **Double-dispose do UnitOfWork**: `AsyncQueryableRepository.Dispose(bool)` chama `base.Dispose()` (que já dispõe o UoW) e dispõe o UoW de novo — causado por campo `_disposed` sombreado na derivada. | `Repository/AsyncQueryableRepository.cs:756-773` + `Repository/QueryableRepository.cs:363-378` | -| C2 | 🔴 | **Ownership invertido do UoW**: repositórios dispõem o UnitOfWork **injetado**; com `AddGenericRepositories` o container também o dispõe → DbContext morto para os demais repositórios do escopo. | `Repository/QueryableRepository.cs:374`; `Read/QueryableReadRepository.cs:22`; `Write/WriteRepository.cs:15` | -| C3 | 🔴 | **Registro de DI quebra em runtime**: `ISqlUnitOfWork` é registrado incondicionalmente mesmo quando a implementação não o implementa (MongoDb) → `InvalidCastException` ao resolver. A linha 75 ainda duplica o registro de `IQueryableUnitOfWork` (código morto). | `Repository/Extensions/ServiceCollectionExtensions.cs:74-75` | -| A1 | 🟠 | **`All`/`Any` (sync) descartam `configuration`**: `return this.All(specification.SatisfiedBy());` ignora includes/no-tracking/sorting. As variantes async fazem certo — prova de que é bug, não design. | `Read/QueryableReadRepository.cs:208,233` | -| A2 | 🟠 | **`Get(id)` rejeita chaves default válidas com exceção errada**: `if (Equals(id, default(TKey))) throw new ArgumentNullException` — `Get(0)`/`Guid.Empty` lançam sobre um argumento não-nulo. | `Read/QueryableReadRepository.cs:254-257`; `Read/AsyncQueryableReadRepository.cs:357-360` | -| A3 | 🟠 | **Sync deferred vs async materializado**: `GetAll`/`GetFiltered`/`GetPaged` sync devolvem `IQueryable` viva disfarçada de `IEnumerable` (dupla enumeração = 2 queries; `ObjectDisposedException` tardia), enquanto os async fazem `ToListAsync`. Mesmo método, semânticas divergentes. | `Read/QueryableReadRepository.cs:47,271,299,396` vs `Read/AsyncQueryableReadRepository.cs:44,336,732` | -| A4 | 🟠 | **Chave como `Expression.Constant`**: `GetFindByKeyExpression` embute o valor da chave na árvore → EF não parametriza; cada id gera entrada nova no cache de queries e SQL com literal (poluição do plan cache). | `Repository/Extensions/DbContextExtensions.cs:25,43` | -| A5 | 🟠 | **Paginação sem `OrderBy`**: `Skip/Take` sem ordenação garantida → páginas não determinísticas + warning `RowLimitingOperationWithoutOrderBy` do EF. | `Read/QueryableReadRepository.cs:395`; `Read/AsyncQueryableReadRepository.cs:729` | -| M-core | 🟡 | Vários: NRT desabilitado no pacote inteiro; **zero `ConfigureAwait(false)`** em toda a biblioteca; `SumAsync` (≈24 overloads) sem `CancellationToken` nem validação de null; `AddRepository` ignora o lifetime configurado; reflection sem cache no caminho quente de `Get(id, config)`; `GetAllAsync` injeta `Where(_ => true)` redundante; tracking inconsistente entre `Get(id)` (usa `Find`) e `Get(id, config)` (usa query). | ver relatório detalhado por arquivo | - -### 3. Bugs de correção — Providers - -| # | Sev. | Problema | Local | +| C1 | 🔴 | **UnitOfWork double-dispose**: `AsyncQueryableRepository.Dispose(bool)` calls `base.Dispose()` (which already disposes the UoW) and disposes the UoW again — caused by a shadowed `_disposed` field on the derived class. | `Repository/AsyncQueryableRepository.cs:756-773` + `Repository/QueryableRepository.cs:363-378` | +| C2 | 🔴 | **Inverted UoW ownership**: repositories dispose the **injected** UnitOfWork; with `AddGenericRepositories` the container disposes it too → dead DbContext for the other repositories in the scope. | `Repository/QueryableRepository.cs:374`; `Read/QueryableReadRepository.cs:22`; `Write/WriteRepository.cs:15` | +| C3 | 🔴 | **DI registration throws at runtime**: `ISqlUnitOfWork` is registered unconditionally even when the implementation does not implement it (MongoDb) → `InvalidCastException` on resolve. Line 75 also duplicates the `IQueryableUnitOfWork` registration (dead code). | `Repository/Extensions/ServiceCollectionExtensions.cs:74-75` | +| A1 | 🟠 | **Sync `All`/`Any` discard `configuration`**: `return this.All(specification.SatisfiedBy());` ignores includes/no-tracking/sorting. The async variants do it right — proof it is a bug, not design. | `Read/QueryableReadRepository.cs:208,233` | +| A2 | 🟠 | **`Get(id)` rejects valid default keys with the wrong exception**: `if (Equals(id, default(TKey))) throw new ArgumentNullException` — `Get(0)`/`Guid.Empty` throw on a non-null argument. | `Read/QueryableReadRepository.cs:254-257`; `Read/AsyncQueryableReadRepository.cs:357-360` | +| A3 | 🟠 | **Sync deferred vs async materialized**: sync `GetAll`/`GetFiltered`/`GetPaged` return a live `IQueryable` disguised as `IEnumerable` (double enumeration = 2 queries; late `ObjectDisposedException`), while the async ones do `ToListAsync`. Same method, divergent semantics. | `Read/QueryableReadRepository.cs:47,271,299,396` vs `Read/AsyncQueryableReadRepository.cs:44,336,732` | +| A4 | 🟠 | **Key as `Expression.Constant`**: `GetFindByKeyExpression` embeds the key value in the tree → EF does not parameterize; each id creates a new query-cache entry and SQL with a literal (plan-cache pollution). | `Repository/Extensions/DbContextExtensions.cs:25,43` | +| A5 | 🟠 | **Pagination without `OrderBy`**: `Skip/Take` with no guaranteed ordering → non-deterministic pages + EF's `RowLimitingOperationWithoutOrderBy` warning. | `Read/QueryableReadRepository.cs:395`; `Read/AsyncQueryableReadRepository.cs:729` | +| M-core | 🟡 | Several: NRT disabled across the package; **zero `ConfigureAwait(false)`** in the whole library; `SumAsync` (~24 overloads) with no `CancellationToken` and no null validation; `AddRepository` ignores the configured lifetime; uncached reflection on the hot `Get(id, config)` path; `GetAllAsync` injects a redundant `Where(_ => true)`; inconsistent tracking between `Get(id)` (uses `Find`) and `Get(id, config)` (uses a query). | see the per-file detailed report | + +### 3. Correctness bugs — Providers + +| # | Sev. | Problem | Location | |---|------|----------|-------| -| P1 | 🔴 | **MongoDb opera no banco errado**: `GetDatabase(_collectionName)` usa o nome da **coleção** como nome do **banco** → `DeleteMany`/`UpdateMany` executam contra banco inexistente e retornam `0` **silenciosamente**. | `MongoDb/Repository/Set.cs:129-131` | -| P2 | 🔴 | **MongoDb `UpdateDefinitionBuilder` grava constante**: `x => new E { Count = x.Count + 1 }` é compilado e invocado contra `Activator.CreateInstance(...)` (instância default) → grava `0+1=1` em todos os documentos, em vez de incrementar. Corrupção silenciosa. | `MongoDb/UpdateDefinitionBuilder.cs:38-43` | -| P3 | 🟠 | **`EXEC` (T-SQL) copiado para PG/MySQL**: `GetQueryProcedure` retorna `$"EXEC {name}..."`; PostgreSQL/MySQL exigem `CALL` → `ExecuteProcedure` falha em runtime. Evidência direta do copy-paste. | `PostgreSql/Repository/SqlExecutor.cs:367`; `MySql/…:367` | -| P4 | 🟠 | **`FromSqlRaw` recebe `IEnumerable` como 1 parâmetro**: `FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value))` — o `Select` vira um único elemento do `params object[]`. Falta `.ToArray()`. | `SqlExecutor.cs:219` (3 providers SQL) | -| P5 | 🟠 | **`ExpressionConverter` frágil**: `RewriteBinding` não faz rebind do parâmetro → updates que referenciam a entidade lançam em runtime; `GetMethods().…Single(...)` quebra se o EF adicionar overload de `SetProperty`; sem cache. | `SqlServer/ExpressionConverter.cs:50-68,137-155` (3 providers SQL) | -| P6 | 🟡 | **MongoDb: retornos silenciosos**: sem `IMongoClient` no DI, `GetCollection()` retorna null e os bulk ops retornam `0` sem lançar. `IsSimpleType` não cobre `Guid`/`DateTimeOffset`/coleções → updates descartados ou `TargetParameterCountException`. | `MongoDb/Repository/Set.cs:29-65`; `UpdateDefinitionBuilder.cs:45-73` | -| P7 | 🟡 | **`SqlExecutor.Dispose` destrói o `DbContext` injetado** (double-dispose no escopo DI). `IsMigrating` é `static` compartilhado entre todos os contextos do processo. Loop `do/while` de retry sem limite em `CommitAndRefreshChanges`. | `SqlExecutor.cs:483-497`; `UnitOfWork.cs:17,43-91` | -| P8 | 🟡 | **`GetEntityByIdSpecification` órfão**: existe só no SqlServer, nada nele é específico de SQL Server (delega para `DbContextExtensions` do base), zero usos no repo. Deveria estar no base ou ser deprecado. | `SqlServer/Specifications/GetEntityByIdSpecification.cs` | - -### 4. Duplicação estrutural - -| Arquivo | SqlServer | PostgreSql | MySql | MongoDb | +| P1 | 🔴 | **MongoDb operates against the wrong database**: `GetDatabase(_collectionName)` uses the **collection** name as the **database** name → `DeleteMany`/`UpdateMany` run against a non-existent database and return `0` **silently**. | `MongoDb/Repository/Set.cs:129-131` | +| P2 | 🔴 | **MongoDb `UpdateDefinitionBuilder` writes a constant**: `x => new E { Count = x.Count + 1 }` is compiled and invoked against `Activator.CreateInstance(...)` (a default instance) → writes `0+1=1` on every document instead of incrementing. Silent corruption. | `MongoDb/UpdateDefinitionBuilder.cs:38-43` | +| P3 | 🟠 | **`EXEC` (T-SQL) copied to PG/MySQL**: `GetQueryProcedure` returns `$"EXEC {name}..."`; PostgreSQL/MySQL require `CALL` → `ExecuteProcedure` fails at runtime. Direct evidence of the copy-paste. | `PostgreSql/Repository/SqlExecutor.cs:367`; `MySql/…:367` | +| P4 | 🟠 | **`FromSqlRaw` receives an `IEnumerable` as 1 parameter**: `FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value))` — the `Select` becomes a single element of the `params object[]`. Missing `.ToArray()`. | `SqlExecutor.cs:219` (3 SQL providers) | +| P5 | 🟠 | **Fragile `ExpressionConverter`**: `RewriteBinding` does not rebind the parameter → updates referencing the entity throw at runtime; `GetMethods().…Single(...)` breaks if EF adds a `SetProperty` overload; no cache. | `SqlServer/ExpressionConverter.cs:50-68,137-155` (3 SQL providers) | +| P6 | 🟡 | **MongoDb: silent returns**: without an `IMongoClient` in DI, `GetCollection()` returns null and the bulk ops return `0` without throwing. `IsSimpleType` does not cover `Guid`/`DateTimeOffset`/collections → discarded updates or `TargetParameterCountException`. | `MongoDb/Repository/Set.cs:29-65`; `UpdateDefinitionBuilder.cs:45-73` | +| P7 | 🟡 | **`SqlExecutor.Dispose` destroys the injected `DbContext`** (double-dispose in the DI scope). `IsMigrating` is a `static` shared across every context in the process. Unbounded `do/while` retry loop in `CommitAndRefreshChanges`. | `SqlExecutor.cs:483-497`; `UnitOfWork.cs:17,43-91` | +| P8 | 🟡 | **Orphaned `GetEntityByIdSpecification`**: exists only in SqlServer, nothing in it is SQL Server-specific (it delegates to the base `DbContextExtensions`), zero uses in the repo. Should be in the base or deprecated. | `SqlServer/Specifications/GetEntityByIdSpecification.cs` | + +### 4. Structural duplication + +| File | SqlServer | PostgreSql | MySql | MongoDb | |---|---|---|---|---| -| `SqlExecutor.cs` | 498 linhas | **idêntico** | **idêntico** | — | -| `UnitOfWork.cs` | 266 | **idêntico** | **idêntico** | ~180 iguais | -| `ExpressionConverter.cs` | 175 | **idêntico** | **idêntico** | — | -| `Set.cs` | 287 | 268 (= SqlServer sem blocos legados) | **idêntico ao PG** | ~50 iguais | -| `SqlConfigurationExtensions.cs` | 14 | **idêntico** | **idêntico** | — | +| `SqlExecutor.cs` | 498 lines | **identical** | **identical** | — | +| `UnitOfWork.cs` | 266 | **identical** | **identical** | ~180 same | +| `ExpressionConverter.cs` | 175 | **identical** | **identical** | — | +| `Set.cs` | 287 | 268 (= SqlServer minus the legacy blocks) | **identical to PG** | ~50 same | +| `SqlConfigurationExtensions.cs` | 14 | **identical** | **identical** | — | -**~2.400–2.500 linhas redundantes.** As únicas diferenças genuínas de dialeto em `SqlExecutor` são 2 linhas -(`GetQueryFunction`/`GetQueryProcedure`). Não é dívida só estética: o bug P3 (`EXEC`→`CALL`) existe -justamente porque o arquivo foi copiado sem adaptar o dialeto. +**~2,400–2,500 redundant lines.** The only genuine dialect differences in `SqlExecutor` are 2 lines +(`GetQueryFunction`/`GetQueryProcedure`). It is not merely cosmetic debt: the P3 bug (`EXEC`→`CALL`) exists +precisely because the file was copied without adapting the dialect. -### 5. Packaging e versionamento +### 5. Packaging and versioning -| # | Sev. | Problema | Local | +| # | Sev. | Problem | Location | |---|------|----------|-------| -| PK1 | 🔴 | **Linhas de versão paralelas no mesmo `PackageId`**: 4.x multi-target + 6.x/7.x/8.x/9.x/10.x single-target. O NuGet vê uma linha do tempo única → `dotnet add package` puxa 10.0.2 (net10-only) e quebra restore em net6–net9; Dependabot sugere upgrade impossível; major deixa de significar breaking (significa TFM). | 21 csproj em `src/`; confirmado no nuget.org | -| PK2 | 🔴 | **Bug de grafo de dependência**: `MySql.Net10.csproj` referencia o core **Net9** → o pacote MySql 10.0.2 declara dependência do core `>= 9.1.2` (net9-only). SqlServer/PG/MongoDb NetX referenciam o core multi-target 4.4.2 — famílias já cruzadas. | `MySql/…MySql.Net10.csproj:66-67` | -| PK3 | 🔴 | **Pomelo 9 sobre EF Core 10**: MySql net10.0 usa `Pomelo.EntityFrameworkCore.MySql 9.0.0` (compilado p/ EF 9) com `Microsoft.EntityFrameworkCore 10.0.3`. Não há Pomelo 10 estável — risco de incompatibilidade binária. | `MySql/…MySql.csproj` (bloco net10) | -| PK4 | 🟠 | **Diretórios `obj/`/`bin/` compartilhados**: vários csproj na mesma pasta sem `BaseIntermediateOutputPath` → `project.assets.json` sobrescrito a cada restore; builds paralelos (`dotnet build -m` da solution) são corrida declarada. | `src/*/` com múltiplos csproj | -| PK5 | 🟠 | **MongoDb principal desalinhado**: `Version 8.1.2.0`, só `net8.0` — não há MongoDb na família 4.x nem multi-target; consumidor net9/net10 recebe o build net8. | `MongoDb/…MongoDb.csproj:7,9` | -| PK6 | 🟠 | **`AssemblyVersion` rotativa** (muda a cada patch) → num diamante entre providers compilados contra linhas diferentes do core, risco de `MissingMethodException`/`FileLoadException`. | todos os csproj `:27` | -| PK7 | 🟡 | **MSBump morto e quebrado**: `build/MSBump.props` importa a si mesmo (circular), `MSBump.targets` chama `BumpVersion` sem `UsingTask`, `Directory.Build.targets` está em `build/` (não é ancestral de `src/`, nunca é aplicado) e o próprio comentário diz que é obsoleto desde NuGet 4.6. Nada disso é importado hoje. Quando funcionava, gerava versões não determinísticas por build. | `build/*` | +| PK1 | 🔴 | **Parallel version lines on the same `PackageId`**: 4.x multi-target + 6.x/7.x/8.x/9.x/10.x single-target. NuGet sees a single timeline → `dotnet add package` pulls 10.0.2 (net10-only) and breaks restore on net6–net9; Dependabot suggests an impossible upgrade; the major stops meaning "breaking" (it means TFM). | 21 csproj in `src/`; confirmed on nuget.org | +| PK2 | 🔴 | **Dependency-graph bug**: `MySql.Net10.csproj` references the **Net9** core → the MySql 10.0.2 package declares a dependency on core `>= 9.1.2` (net9-only). SqlServer/PG/MongoDb NetX reference the multi-target core 4.4.2 — the families are already crossed. | `MySql/…MySql.Net10.csproj:66-67` | +| PK3 | 🔴 | **Pomelo 9 over EF Core 10**: MySql net10.0 uses `Pomelo.EntityFrameworkCore.MySql 9.0.0` (built for EF 9) with `Microsoft.EntityFrameworkCore 10.0.3`. There is no stable Pomelo 10 — risk of binary incompatibility. | `MySql/…MySql.csproj` (net10 block) | +| PK4 | 🟠 | **Shared `obj/`/`bin/` directories**: several csproj files in the same folder without `BaseIntermediateOutputPath` → `project.assets.json` overwritten on each restore; parallel builds (`dotnet build -m` of the solution) are a declared race. | `src/*/` with multiple csproj | +| PK5 | 🟠 | **MongoDb main package misaligned**: `Version 8.1.2.0`, `net8.0` only — there is no MongoDb in the 4.x family or a multi-target one; a net9/net10 consumer gets the net8 build. | `MongoDb/…MongoDb.csproj:7,9` | +| PK6 | 🟠 | **Rolling `AssemblyVersion`** (changes on each patch) → in a diamond between providers compiled against different core lines, risk of `MissingMethodException`/`FileLoadException`. | all csproj `:27` | +| PK7 | 🟡 | **Dead and broken MSBump**: `build/MSBump.props` imports itself (circular), `MSBump.targets` calls `BumpVersion` with no `UsingTask`, `Directory.Build.targets` is in `build/` (not an ancestor of `src/`, never applied), and its own comment says it is obsolete since NuGet 4.6. None of it is imported today. When it worked, it produced non-deterministic per-build versions. | `build/*` | -### 6. Contratos (`eQuantic.Core.Data`) +### 6. Contracts (`eQuantic.Core.Data`) -| # | Sev. | Problema | Local | +| # | Sev. | Problem | Location | |---|------|----------|-------| -| K1 | 🟠 | **Explosão combinatória**: `IAsyncReadRepository` = **100 membros**; `IReadRepository` = 56; `ISqlUnitOfWork` ≈ 47 (inviável implementar à mão — anula o propósito de um contrato). `GetPagedAsync` = 18 overloads; `SumAsync` = 30. O commit mais recente *adicionou* 60 overloads de Sum — a tendência é piorar. | `Read/IAsyncReadRepository.cs`; `Sql/ISqlUnitOfWork.cs` | -| K2 | 🟠 | **`TUnitOfWork` como type parameter** habilita um único membro (`UnitOfWork { get; }`) mas contamina toda a hierarquia (~24 interfaces para 1 conceito) e cria acoplamento circular UoW↔repositório. Variância inconsistente sync vs async. | `Repository/IRepository.cs:26,55`; `IAsyncRepository.cs:39` | -| K3 | 🟠 | **Paginação sem metadados**: `GetPaged*` retorna `IEnumerable` cru, sem total/página → consumidor faz `Count()` separado (2 round-trips não atômicas). Falta um `PagedResult`. | `Read/IReadRepository.cs:267-314` | -| K4 | 🟡 | **Vazamento de EF no contrato**: `ISqlUnitOfWork` declara `GetPendingMigrations`/`UpdateDatabase`/`Attach` — contradiz a "persistence ignorance" que os próprios XML docs reivindicam. `IdentityGenerator` e `MigrationAttribute` são implementação num pacote de contratos. | `Sql/ISqlUnitOfWork.cs:23-127`; `IdentityGenerator.cs`; `Migration/MigrationAttribute.cs` | -| K5 | 🟡 | **`CancellationToken` ausente** em 30 `SumAsync`, `AddAsync`/`MergeAsync`/`ModifyAsync`/`RemoveAsync`, `LoadCollectionAsync`. NRT desabilitado (`Get*` retorna `Task` sem anotar null). Sem `IAsyncEnumerable`/streaming. | `Read/IAsyncReadRepository.cs`; `Write/IAsyncWriteRepository.cs` | -| K6 | 🟡 | **Constraint `new()` em tudo** (hostil a DDD) e `IEntity`/`IEntity` não ligam o `TKey` do repositório à chave real da entidade (`IRepository` compila mesmo se a chave for `int`). Deveria ser `where TEntity : IEntity`. | `Repository/IRepository.cs:40` | -| K7 | 🟢 | **Bug latente**: `IdentityGenerator.GuidRegex` contém 2 caracteres invisíveis de largura zero (U+200C e U+200B) dentro da classe `[0-9…a-fA-F]{12}` — artefato de copy-paste (confirmado por dump de bytes). Dependência morta `eQuantic.Core` (nenhum arquivo a importa). Typo `mintute` em `MigrationAttribute`. | `IdentityGenerator.cs:7`; `core-data.csproj` | +| K1 | 🟠 | **Combinatorial explosion**: `IAsyncReadRepository` = **100 members**; `IReadRepository` = 56; `ISqlUnitOfWork` ≈ 47 (impossible to implement by hand — defeats the purpose of a contract). `GetPagedAsync` = 18 overloads; `SumAsync` = 30. The most recent commit *added* 60 Sum overloads — the trend is worsening. | `Read/IAsyncReadRepository.cs`; `Sql/ISqlUnitOfWork.cs` | +| K2 | 🟠 | **`TUnitOfWork` as a type parameter** enables a single member (`UnitOfWork { get; }`) but contaminates the whole hierarchy (~24 interfaces for 1 concept) and creates a circular UoW↔repository coupling. Inconsistent variance sync vs async. | `Repository/IRepository.cs:26,55`; `IAsyncRepository.cs:39` | +| K3 | 🟠 | **Pagination without metadata**: `GetPaged*` returns raw `IEnumerable`, with no total/page → the consumer does a separate `Count()` (2 non-atomic round-trips). A `PagedResult` is missing. | `Read/IReadRepository.cs:267-314` | +| K4 | 🟡 | **EF leaking into the contract**: `ISqlUnitOfWork` declares `GetPendingMigrations`/`UpdateDatabase`/`Attach` — contradicting the "persistence ignorance" its own XML docs claim. `IdentityGenerator` and `MigrationAttribute` are implementation in a contracts package. | `Sql/ISqlUnitOfWork.cs:23-127`; `IdentityGenerator.cs`; `Migration/MigrationAttribute.cs` | +| K5 | 🟡 | **`CancellationToken` missing** on 30 `SumAsync`, `AddAsync`/`MergeAsync`/`ModifyAsync`/`RemoveAsync`, `LoadCollectionAsync`. NRT disabled (`Get*` returns `Task` with no null annotation). No `IAsyncEnumerable`/streaming. | `Read/IAsyncReadRepository.cs`; `Write/IAsyncWriteRepository.cs` | +| K6 | 🟡 | **`new()` constraint everywhere** (hostile to DDD) and `IEntity`/`IEntity` do not tie the repository's `TKey` to the entity's real key (`IRepository` compiles even if the key is `int`). Should be `where TEntity : IEntity`. | `Repository/IRepository.cs:40` | +| K7 | 🟢 | **Latent bug**: `IdentityGenerator.GuidRegex` contains 2 invisible zero-width characters (U+200C and U+200B) inside the `[0-9…a-fA-F]{12}` class — a copy-paste artifact (confirmed by a byte dump). Dead `eQuantic.Core` dependency (no file imports it). Typo `mintute` in `MigrationAttribute`. | `IdentityGenerator.cs:7`; `core-data.csproj` | -### 7. Processo, CI e testes +### 7. Process, CI and tests -| # | Sev. | Problema | Local | +| # | Sev. | Problem | Location | |---|------|----------|-------| -| Q1 | 🔴 | **CI publica em qualquer push, sem testes**: `on: [push]` → `dotnet nuget push` a cada push em qualquer branch, com a chave NuGet. **Nenhum `dotnet test`** roda antes de publicar. | `.github/workflows/dotnetcore.yml:3,66-67` | -| Q2 | 🔴 | **Testes são placebo**: `UnitTest1.cs` é um `Assert.Pass()`. Não há cobertura de nenhum bug acima. | `tests/…Tests/UnitTest1.cs` | -| Q3 | 🟡 | 21 `dotnet build` sequenciais em vez da solution; ações desatualizadas (`checkout@v3`, `setup-dotnet@v3`); `windows-latest` desnecessário; sem `dotnet test`, cache, `global.json`, pack determinístico (`ContinuousIntegrationBuild`), símbolos (snupkg) ou provenance. | `dotnetcore.yml` | -| Q4 | 🟡 | Sem `Directory.Build.props`/`Directory.Packages.props` centrais: metadados e versões de pacote repetidos em 21 csproj (fonte real de PK2/PK3). Sem NRT/`GenerateDocumentationFile` consistentes (pacotes vão ao NuGet sem IntelliSense). README diz "Version 4.4.0" e não explica a matriz de versões; `Repository.md` usa `IContainer`/service-locator pré-DI. | raiz do repo | +| Q1 | 🔴 | **CI publishes on any push, without tests**: `on: [push]` → `dotnet nuget push` on every push to any branch, with the NuGet key. **No `dotnet test`** runs before publishing. | `.github/workflows/dotnetcore.yml:3,66-67` | +| Q2 | 🔴 | **Placebo tests**: `UnitTest1.cs` is an `Assert.Pass()`. There is no coverage of any bug above. | `tests/…Tests/UnitTest1.cs` | +| Q3 | 🟡 | 21 sequential `dotnet build` steps instead of the solution; outdated actions (`checkout@v3`, `setup-dotnet@v3`); unnecessary `windows-latest`; no `dotnet test`, cache, `global.json`, deterministic pack (`ContinuousIntegrationBuild`), symbols (snupkg) or provenance. | `dotnetcore.yml` | +| Q4 | 🟡 | No central `Directory.Build.props`/`Directory.Packages.props`: metadata and package versions repeated across 21 csproj (the real source of PK2/PK3). No consistent NRT/`GenerateDocumentationFile` (packages ship to NuGet with no IntelliSense). The README says "Version 4.4.0" and does not explain the version matrix; `Repository.md` uses `IContainer`/pre-DI service-locator. | repo root | --- -## Parte II — Plano de execução em fases +## Part II — Phased execution plan + +### Phase 0 — Harden the pipeline (days, no production code) — **done** + +Prerequisite for everything: stop publishing by accident and have a safety net. -### Fase 0 — Blindar o pipeline (dias, sem tocar código de produção) +1. **Split CI from Release.** `ci.yml` on `push`/`pull_request`: `restore` → `build` → **`dotnet test`** → + `dotnet pack` as an artifact (no push). `release.yml` only on `push: tags: ['v*']`, with the + `nuget_key` in a **protected GitHub Environment**. +2. Update the actions to v4; move to `ubuntu-latest`; add a NuGet cache and `global.json`. +3. **Delete `build/`** (dead MSBump). MinVer adoption is deferred until the versioning decision (Part IV). -Pré-requisito de tudo: parar de publicar por acidente e ter uma rede de segurança. +### Phase 1 — Non contract-breaking fixes (current line, patch/minor) — **done** -1. **Separar CI de Release.** `ci.yml` em `push`/`pull_request`: `restore` → `build -warnaserror` → **`dotnet test`** → `dotnet pack` como artefato (sem push). `release.yml` só em `push: tags: ['v*']` (ou `release: published`), com a chave `nuget_key` num **GitHub Environment protegido**. -2. **Trocar os 21 builds** por `dotnet build eQuantic.Core.Data.EntityFramework.sln -c Release` (ou `dotnet pack`); mudar para `ubuntu-latest`; atualizar ações para v4; adicionar cache NuGet e `global.json`. -3. **Adotar MinVer** (versão derivada de tag git) e **deletar `build/`** (MSBump morto). Fixar `AssemblyVersion` por major. +Ship now, without touching `eQuantic.Core.Data`. Each covered by a test. See the status table above. -### Fase 1 — Correções que não quebram contrato (linha atual, patch/minor) +### Phase 2 — Structural de-duplication (minor, internal refactor) — **done** -Podem sair já, sem tocar o `eQuantic.Core.Data`. Cobrir cada uma com teste (Fase 0 garante que rodam). +Extracted the shared relational implementation into a new +`eQuantic.Core.Data.EntityFramework.Relational` package (see the status section above for why a new package +rather than the base package). Each provider becomes thin subclasses + a dialect override. Removes ~2,200 +lines and kills the P3 bug class at the root. **Consumer-facing types keep their namespaces**; only a few +implementation-only types move. -- **Segurança S1:** parametrizar `SqlExecutor` (usar a infra `SetCommand` já existente). -- **Crash C3:** registrar `ISqlUnitOfWork` só se a impl o implementar; remover o registro duplicado. -- **Disposal C1/C2/P7:** unificar o flag `_disposed`, não dispor o UoW injetado (ownership de quem cria), respeitar o escopo do DI. -- **Correção de queries A1, A2, A4, A5, P4:** repassar `configuration` em `All`/`Any`; validar `id is null` em vez de `default`; parametrizar a chave; fallback de `OrderBy` pela PK; `.ToArray()` no `FromSqlRaw`. -- **MongoDb P1/P2/P6:** corrigir o `GetDatabase`, rejeitar (ou traduzir para `$inc`) updates que referenciam a entidade, lançar em vez de retornar `0` silencioso. -- **Dialeto P3:** `EXEC`→`CALL` em PG/MySql. -- **Higiene M-core:** `ConfigureAwait(false)`, cache da expressão de chave em `ConcurrentDictionary`, `AddRepository` respeitando lifetime. -- **PK2/PK3/PK5:** corrigir a referência do `MySql.Net10` para o core net10; alinhar MongoDb; documentar/pinar o risco Pomelo↔EF10. -- **K7 (contrato, não-breaking):** remover os caracteres zero-width da regex, a dependência morta `eQuantic.Core`, adicionar `[AttributeUsage]`/`GenerateDocumentationFile`. +### Phase 3 — Contracts redesign v5.0.0 (deliberately breaking — see Part III) -### Fase 2 — De-duplicação estrutural (minor, refactor interno) +Consolidate the surface via *options objects*, introduce `PagedResult`, a uniform `CancellationToken`, +annotated NRT, `where TEntity : IEntity`, remove `TUnitOfWork` from the hierarchy and the EF-specific +content from the contracts. Reimplement on the EF package (which becomes ~10× smaller). -Criar no pacote base `eQuantic.Core.Data.EntityFramework`: -- `SqlExecutorBase` com `GetQueryFunction`/`GetQueryProcedure` `protected abstract` (dialeto) — colapsa ~500×2 linhas. -- `UnitOfWorkBase`/`UnitOfWorkBase`, `ExpressionConverter` e o `GetQueryable`/`Load*` de `Set` no base. -- Mover `GetEntityByIdSpecification` (P8) para o base. +### Phase 4 — Migration on nuget.org (see Part IV) -Cada provider passa a ser só o override de dialeto + o `csproj`. Remove ~2.400 linhas e mata a classe de bug do P3 na raiz. **Não muda API pública** — só reorganiza a implementação. +Consolidate the version lines, deprecate the old ones without breaking anyone who already depends on them, +and publish the compatibility matrix. -### Fase 3 — Redesenho dos contratos v5.0.0 (breaking deliberado — ver Parte III) +--- + +## Part III — Changes requiring a contract break in `eQuantic.Core.Data` -Consolidar a superfície via *options objects*, introduzir `PagedResult`, `CancellationToken` uniforme, -NRT anotado, `where TEntity : IEntity`, remover `TUnitOfWork` da hierarquia e o conteúdo -EF-specific dos contratos. Reimplementar no pacote EF (que fica ~10× menor). +General rule for the contracts package: **adding** a member to an interface already breaks every external +implementer (mocks, fakes, decorators, plus the EF impl itself), and **changing a signature/removing** +breaks callers too. Almost every fundamental improvement is therefore a v5.0.0. What requires breaking: -### Fase 4 — Migração no nuget.org (ver Parte IV) +1. **`CancellationToken` on the members that lack it** (30 `SumAsync`, `AddAsync`/`MergeAsync`/`ModifyAsync`/`RemoveAsync`, `LoadCollectionAsync`). Partial non-breaking route: *default interface methods* (DIM) delegating to the existing overload — viable because all TFMs are ≥ net6.0, but it crystallizes the overload explosion. +2. **Consolidate overloads into options objects** (`QueryOptions` absorbing filter/specification/config; `PageRequest`): remove the 18 `GetPagedAsync`, the 60 `Sum*` etc. Reduces `IAsyncReadRepository` from 100 to ~12 members. This is the heart of v5. +3. **`PagedResult` instead of `IEnumerable`** for pagination: a return-type change — hard breaking (not even DIM saves it; would require a new method with a different name, e.g. `QueryPagedAsync`). +4. **Remove `TUnitOfWork` from the hierarchy** (collapse `IRepository` into `IRepository` + `IUnitOfWork UnitOfWork { get; }`): removes ~10 public interfaces; the EF package references those arities in `GetRepository`. +5. **`where TEntity : IEntity` constraint** and/or removing `new()`: changes generic constraints — source+binary breaking. +6. **Split sync/async in `IUnitOfWork`** and **remove `ExecuteTransactionAsync` from `ISqlExecutor`** (an async method on the "sync" interface). +7. **Move `GetPendingMigrations`/`UpdateDatabase`/`MigrationAttribute`/`IdentityGenerator` to the EF package**: removes public types/members from the contract — a real break (the contract→EF direction rules out `[TypeForwardedTo]`). +8. **Annotated NRT** (`TEntity?` on `Get`/`GetFirst`/`GetSingle`): technically only produces new warnings — the cheapest break; requires both packages to be annotated together to stay consistent. -Consolidar as linhas de versão, deprecar as antigas sem quebrar quem já depende delas, e publicar a matriz -de compatibilidade. +**Recommendation:** treat the next contract version as a **deliberately breaking v5.0.0** and reimplement +the EF package on top of it, rather than stacking DIMs over a 100-member surface. The EF package's +maintenance cost — today implementing ~156 members per provider × 4 providers — drops by an order of +magnitude. --- -## Parte III — Mudanças que exigem quebra de contrato no `eQuantic.Core.Data` +## Part IV — Versioning strategy on nuget.org -Regra geral do pacote de contratos: **adicionar** membro a uma interface já quebra todo implementador -externo (mocks, fakes, decorators, além da própria impl EF), e **mudar assinatura/remover** quebra também -os callers. Quase toda melhoria de fundo é, portanto, uma v5.0.0. O que exige breaking: +The PK1 problem has two coherent exits. The recommended one is (A). -1. **`CancellationToken` nos membros que não têm** (30 `SumAsync`, `AddAsync`/`MergeAsync`/`ModifyAsync`/`RemoveAsync`, `LoadCollectionAsync`). Rota não-breaking parcial: *default interface methods* (DIM) delegando para a sobrecarga existente — viável porque todos os TFMs são ≥ net6.0, mas cristaliza a explosão de overloads. -2. **Consolidar overloads em options objects** (`QueryOptions` absorvendo filter/specification/config; `PageRequest`): remover os 18 `GetPagedAsync`, os 60 `Sum*` etc. Reduz `IAsyncReadRepository` de 100 para ~12 membros. É o coração da v5. -3. **`PagedResult` em vez de `IEnumerable`** na paginação: mudança de tipo de retorno — breaking duro (nem DIM salva; exigiria método novo com outro nome, ex. `QueryPagedAsync`). -4. **Remover `TUnitOfWork` da hierarquia** (colapsar `IRepository` em `IRepository` + `IUnitOfWork UnitOfWork { get; }`): remove ~10 interfaces públicas; o pacote EF referencia essas aridades em `GetRepository`. -5. **Constraint `where TEntity : IEntity`** e/ou remover `new()`: muda constraints genéricas — source+binary breaking. -6. **Separar sync/async de `IUnitOfWork`** e **remover `ExecuteTransactionAsync` de `ISqlExecutor`** (método async na interface "sync"). -7. **Mover `GetPendingMigrations`/`UpdateDatabase`/`MigrationAttribute`/`IdentityGenerator` para o pacote EF**: remove tipos/membros públicos do contrato — breaking real (a direção contrato→EF impede `[TypeForwardedTo]`). -8. **NRT anotado** (`TEntity?` em `Get`/`GetFirst`/`GetSingle`): tecnicamente só gera warnings novos — o breaking mais barato; exige que os dois pacotes sejam anotados em conjunto para ficarem coerentes. +**(A) Consolidate into a single multi-target line per `PackageId` (recommended).** +One multi-target `.csproj` per package (`net8.0;net9.0;net10.0` — net6/net7 are EOL), a single version line, +resumed **above** the highest already published so the timeline becomes increasing and monotonic again +(e.g. **11.0.0**, or 5.0.0 if you accept the numeric "latest" dropping — which would confuse anyone already +on 10.x). Multi-target already delivers the right binary per TFM inside a single `.nupkg` — exactly what the +parallel-lines scheme tries to emulate by hand. This fixes PK1, PK2, PK4, PK5 and Q4 at once. -**Recomendação:** tratar a próxima versão do contrato como **v5.0.0 deliberadamente breaking** e reimplementar -o pacote EF sobre ela, em vez de empilhar DIMs sobre uma superfície de 100 membros. O custo de manutenção -do pacote EF — que hoje implementa ~156 membros por provider × 4 providers — cai uma ordem de magnitude. +**(B) Keep per-.NET families, but with distinct `PackageId`s.** +E.g. `eQuantic.Core.Data.EntityFramework.Net8`. It is the only way NuGet treats the families as independent +lines (each id's "latest" is correct for its TFM). Cost: it fragments the consumer ecosystem and discovery +on nuget.org, and multiplies the packages to maintain. Only worth it if there is a strong reason to freeze +each TFM to its own API. ---- +**Migration without breaking anyone already on the old versions** (applies to A and B): +- **Never** unpublish (`unlist` keeps restore working for pinned versions; `delete` breaks it). Use + **deprecation** on nuget.org (`Legacy`/`Other`) on the old versions, pointing to the new one. +- Publish the **compatibility matrix** (TFM × package × version) in the README — today the README says + "Version 4.4.0" and explains none of it. +- Align the core + 4 providers to release **always together, at the same version** (resolves the + `AssemblyVersion` diamonds). +- Only then resume the consolidated numbering and point the release CI at tags. -## Parte IV — Estratégia de versionamento no nuget.org - -O problema PK1 tem duas saídas coerentes. A recomendada é a (A). - -**(A) Consolidar numa única linha multi-target por `PackageId` (recomendado).** -Um `.csproj` multi-target por pacote (`net8.0;net9.0;net10.0` — net6/net7 estão EOL), uma única linha de -versão, retomada **acima** da mais alta já publicada para a linha do tempo voltar a ser crescente e -monotônica (ex.: **11.0.0**, ou 5.0.0 se aceitar que a "latest" numérica caia — o que confundiria quem já -está em 10.x). O multi-target já entrega o binário certo por TFM dentro de um único `.nupkg` — é -exatamente o que o esquema de linhas paralelas tenta emular à mão. Isso corrige PK1, PK2, PK4, PK5 e Q4 de -uma vez. - -**(B) Manter famílias por .NET, mas com `PackageId` distintos.** -Ex.: `eQuantic.Core.Data.EntityFramework.Net8`. É a única forma de o NuGet tratar as famílias como linhas -independentes (a "latest" de cada id fica correta para seu TFM). Custo: fragmenta o ecossistema de -consumidores e a descoberta no nuget.org, e multiplica os pacotes a manter. Só vale se houver uma razão -forte para congelar cada TFM numa API própria. - -**Migração sem quebrar quem já depende das versões antigas** (vale para A e B): -- **Nunca** despublicar (`unlist` mantém o restore de quem tem a versão fixada; `delete` quebra). Usar - **deprecação** no nuget.org (`Legacy`/`Other`) nas versões antigas, apontando para a nova. -- Publicar a **matriz de compatibilidade** (TFM × pacote × versão) no README — hoje o README diz - "Version 4.4.0" e não explica nada disso. -- Alinhar core + 4 providers para lançarem **sempre juntos, na mesma versão** (resolve os diamantes de - `AssemblyVersion`). -- Só então retomar a numeração consolidada e apontar o CI de release para tags. +Note: the new `eQuantic.Core.Data.EntityFramework.Relational` package introduced in Phase 2 must join +whichever scheme is chosen here. --- -## Apêndice — Ordem sugerida (o que fazer primeiro) +## Appendix — Suggested order (what to do first) -1. **Fase 0** (pipeline) — desbloqueia tudo com segurança. -2. **S1, C1, C2, C3, P1, P2** — os 6 achados 🔴 de segurança/runtime, com testes. -3. **Fase 1 restante** (achados 🟠) na mesma linha atual. -4. **Fase 2** (de-dup) — barato e alto retorno, sem breaking. -5. **Fases 3–4** — planejar a v5.0.0 do contrato e a consolidação de versões como um marco à parte, - comunicado com antecedência aos consumidores. +1. **Phase 0** (pipeline) — unblocks everything safely. ✅ done +2. **S1, C1, C2, C3, P1, P2** — the 6 🔴 security/runtime findings, with tests. ✅ done +3. **Rest of Phase 1** (🟠 findings) on the current line. ✅ done +4. **Phase 2** (de-dup) — cheap, high return, no break. ✅ done +5. **Phases 3–4** — plan the contract v5.0.0 and the version consolidation as a separate milestone, + communicated to consumers in advance. From f02434395806f95e6efa1227ae931dea18a46141 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 16:20:16 +0000 Subject: [PATCH 20/32] chore: centralize shared package metadata in Directory.Build.props MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Authors, Copyright, project/repository URLs, license/readme/icon, LangVersion and package-output settings were duplicated across all 24 project files. Move them to a root Directory.Build.props and remove the 16 redundant lines from each csproj (384 lines total). Package-specific values (Description, PackageId, Version, TargetFrameworks, tags, dependencies) stay per-project — the per-.NET-major version lines are intentional and unchanged. Verified: all packages build, 28 tests pass, and the packed nuspec keeps its authors/copyright/urls/license/readme/icon and bundled LICENSE/README/Icon files. --- Directory.Build.props | 32 +++++++++++++++++++ ....Data.EntityFramework.MongoDb.Net10.csproj | 16 ---------- ...e.Data.EntityFramework.MongoDb.Net9.csproj | 16 ---------- ...c.Core.Data.EntityFramework.MongoDb.csproj | 16 ---------- ...re.Data.EntityFramework.MySql.Net10.csproj | 16 ---------- ...ore.Data.EntityFramework.MySql.Net8.csproj | 16 ---------- ...ore.Data.EntityFramework.MySql.Net9.csproj | 16 ---------- ...tic.Core.Data.EntityFramework.MySql.csproj | 16 ---------- ...ta.EntityFramework.PostgreSql.Net10.csproj | 16 ---------- ...ata.EntityFramework.PostgreSql.Net8.csproj | 16 ---------- ...ata.EntityFramework.PostgreSql.Net9.csproj | 16 ---------- ...ore.Data.EntityFramework.PostgreSql.csproj | 16 ---------- ...ore.Data.EntityFramework.Relational.csproj | 16 ---------- ...ata.EntityFramework.SqlServer.Net10.csproj | 16 ---------- ...Data.EntityFramework.SqlServer.Net6.csproj | 16 ---------- ...Data.EntityFramework.SqlServer.Net7.csproj | 16 ---------- ...Data.EntityFramework.SqlServer.Net8.csproj | 16 ---------- ...Data.EntityFramework.SqlServer.Net9.csproj | 16 ---------- ...Core.Data.EntityFramework.SqlServer.csproj | 16 ---------- ...tic.Core.Data.EntityFramework.Net10.csproj | 16 ---------- ...ntic.Core.Data.EntityFramework.Net6.csproj | 16 ---------- ...ntic.Core.Data.EntityFramework.Net7.csproj | 16 ---------- ...ntic.Core.Data.EntityFramework.Net8.csproj | 16 ---------- ...ntic.Core.Data.EntityFramework.Net9.csproj | 16 ---------- .../eQuantic.Core.Data.EntityFramework.csproj | 16 ---------- 25 files changed, 32 insertions(+), 384 deletions(-) create mode 100644 Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..58d0fb3 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,32 @@ + + + + + + eQuantic Systems + Copyright © 2016 + https://github.com/eQuantic/core-data-entityframework + https://github.com/eQuantic/core-data-entityframework + Git + LICENSE + README.md + Icon.png + latest + $(MSBuildThisFileDirectory)artifacts/ + true + false + false + false + + + + + + + + + diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj index 63a7812..f753757 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and Mongo DB eQuantic.Core.Data.EntityFramework.MongoDb 10.0.2.0 - eQuantic Systems net10.0 eQuantic.Core.Data.EntityFramework.MongoDb eQuantic.Core.Data.EntityFramework.MongoDb @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 10.0.2.0 10.0.2.0 - Icon.png - latest enable enable @@ -53,8 +39,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj index 3ca09eb..2284b3d 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and Mongo DB eQuantic.Core.Data.EntityFramework.MongoDb 9.1.2.0 - eQuantic Systems net9.0 eQuantic.Core.Data.EntityFramework.MongoDb eQuantic.Core.Data.EntityFramework.MongoDb @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 9.1.2.0 9.1.2.0 - Icon.png - latest enable enable @@ -53,8 +39,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj index fe809e0..7a4e63b 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and Mongo DB eQuantic.Core.Data.EntityFramework.MongoDb 8.1.2.0 - eQuantic Systems net8.0 eQuantic.Core.Data.EntityFramework.MongoDb eQuantic.Core.Data.EntityFramework.MongoDb @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 8.1.2.0 8.1.2.0 - Icon.png - latest enable enable @@ -53,8 +39,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj index 67f3708..c5a9c99 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and MySQL eQuantic.Core.Data.EntityFramework.MySql 10.0.2.0 - eQuantic Systems net10.0 eQuantic.Core.Data.EntityFramework.MySql eQuantic.Core.Data.EntityFramework.MySql @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 10.0.2.0 10.0.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj index 2aa1dc3..2ed130f 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and MySQL eQuantic.Core.Data.EntityFramework.MySql 8.1.2.0 - eQuantic Systems net8.0 eQuantic.Core.Data.EntityFramework.MySql eQuantic.Core.Data.EntityFramework.MySql @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 8.1.2.0 8.1.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj index 0cdc4fd..47d644d 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and MySQL eQuantic.Core.Data.EntityFramework.MySql 9.1.2.0 - eQuantic Systems net9.0 eQuantic.Core.Data.EntityFramework.MySql eQuantic.Core.Data.EntityFramework.MySql @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 9.1.2.0 9.1.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj index afa8549..c3588ed 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and MySQL eQuantic.Core.Data.EntityFramework.MySql 4.4.2.0 - eQuantic Systems net8.0;net9.0;net10.0 eQuantic.Core.Data.EntityFramework.MySql eQuantic.Core.Data.EntityFramework.MySql @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 4.4.2.0 4.4.2.0 - Icon.png - latest @@ -87,8 +73,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj index 8a566ad..80b667a 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and PostgreSQL eQuantic.Core.Data.EntityFramework.PostgreSql 10.0.2.0 - eQuantic Systems net10.0 eQuantic.Core.Data.EntityFramework.PostgreSql eQuantic.Core.Data.EntityFramework.PostgreSql @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 10.0.2.0 10.0.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj index b7aff6e..1a6183a 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and PostgreSQL eQuantic.Core.Data.EntityFramework.PostgreSql 8.1.2.0 - eQuantic Systems net8.0 eQuantic.Core.Data.EntityFramework.PostgreSql eQuantic.Core.Data.EntityFramework.PostgreSql @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 8.1.2.0 8.1.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj index 8258b3f..faaad6e 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and PostgreSQL eQuantic.Core.Data.EntityFramework.PostgreSql 9.1.2.0 - eQuantic Systems net9.0 eQuantic.Core.Data.EntityFramework.PostgreSql eQuantic.Core.Data.EntityFramework.PostgreSql @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 9.1.2.0 9.1.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj index 50b973c..7a96e47 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and PostgreSQL eQuantic.Core.Data.EntityFramework.PostgreSql 4.4.2.0 - eQuantic Systems net8.0;net9.0;net10.0 eQuantic.Core.Data.EntityFramework.PostgreSql eQuantic.Core.Data.EntityFramework.PostgreSql @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 4.4.2.0 4.4.2.0 - Icon.png - latest @@ -85,8 +71,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj index 34f03d8..7af18a1 100644 --- a/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj @@ -5,28 +5,14 @@ Shared relational implementation for eQuantic Core Data Entity Framework providers eQuantic.Core.Data.EntityFramework.Relational 1.0.0.0 - eQuantic Systems net6.0;net7.0;net8.0;net9.0;net10.0 eQuantic.Core.Data.EntityFramework.Relational eQuantic.Core.Data.EntityFramework.Relational eQuantic;Core;Data;Library;Repository;Pattern;SQL;Relational Shared relational base used by the SqlServer, PostgreSql and MySql providers - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 1.0.0.0 1.0.0.0 - Icon.png - latest @@ -57,8 +43,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj index d0a0a11..312361a 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer 10.0.2.0 - eQuantic Systems net10.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 10.0.2.0 10.0.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj index ac1d53d..e0d2206 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer 6.1.2.0 - eQuantic Systems net6.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 6.1.2.0 6.1.2.0 - Icon.png - latest @@ -53,8 +39,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj index de1e210..4cfbda9 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer 7.1.2.0 - eQuantic Systems net7.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 7.1.2.0 7.1.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj index e8378a6..16b5a0b 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer 8.1.2.0 - eQuantic Systems net8.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 8.1.2.0 8.1.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj index ed16f09..887c7c4 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer 9.1.2.0 - eQuantic Systems net9.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 9.1.2.0 9.1.2.0 - Icon.png - latest @@ -52,8 +38,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj index eec1ef5..5515144 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer 4.4.2.0 - eQuantic Systems net6.0;net7.0;net8.0;net9.0;net10.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 4.4.2.0 4.4.2.0 - Icon.png - latest @@ -132,8 +118,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj index faf7e27..467df6b 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework 10.0.2.0 - eQuantic Systems net10.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 10.0.2.0 10.0.2.0 - Icon.png - latest @@ -49,8 +35,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj index 36f3830..3114d85 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework 6.1.2.0 - eQuantic Systems net6.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 6.1.2.0 6.1.2.0 - Icon.png - latest @@ -49,8 +35,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj index 988c99a..95c6ca2 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework 7.1.2.0 - eQuantic Systems net7.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 7.1.2.0 7.1.2.0 - Icon.png - latest @@ -49,8 +35,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj index 4ffe261..13675ee 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework 8.1.2.0 - eQuantic Systems net8.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 8.1.2.0 8.1.2.0 - Icon.png - latest @@ -49,8 +35,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj index b6e5029..e5660aa 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework 9.1.2.0 - eQuantic Systems net9.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 9.1.2.0 9.1.2.0 - Icon.png - latest @@ -49,8 +35,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj index 5db132e..508f0a8 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj @@ -5,7 +5,6 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework 4.4.2.0 - eQuantic Systems net6.0;net7.0;net8.0;net9.0;net10.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 4.4.2.0 4.4.2.0 - Icon.png - latest @@ -100,8 +86,6 @@ - - From a1ff9f6de3527223e5559e1bd04a1147e6e8822b Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 15:42:56 +0100 Subject: [PATCH 21/32] =?UTF-8?q?=F0=9F=94=A7=20chore:=20restructure=20pac?= =?UTF-8?q?kages=20to=20per-major=20(8/10)=20and=20multi-framework=20(4.x)?= =?UTF-8?q?=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop net6/net7/net9; keep net8 and net10. Per-major publish packages (SqlServer/PostgreSql/MySql/MongoDb + base) versioned 8.2.0/10.1.0; multi-framework base (4.5.0) and new Relational (4.0.0) kept in the major-4 lane so they are not confused with a .NET version. All reference eQuantic.Core.Data 5.1.0. --- eQuantic.Core.Data.EntityFramework.sln | 291 +++--------------- ....Data.EntityFramework.MongoDb.Net10.csproj | 8 +- ....Data.EntityFramework.MongoDb.Net8.csproj} | 8 +- ...e.Data.EntityFramework.MongoDb.Net9.csproj | 54 ---- ...re.Data.EntityFramework.MySql.Net10.csproj | 8 +- ...ore.Data.EntityFramework.MySql.Net8.csproj | 8 +- ...ore.Data.EntityFramework.MySql.Net9.csproj | 55 ---- ...tic.Core.Data.EntityFramework.MySql.csproj | 90 ------ ...ta.EntityFramework.PostgreSql.Net10.csproj | 8 +- ...ata.EntityFramework.PostgreSql.Net8.csproj | 8 +- ...ata.EntityFramework.PostgreSql.Net9.csproj | 55 ---- ...ore.Data.EntityFramework.PostgreSql.csproj | 88 ------ ...ore.Data.EntityFramework.Relational.csproj | 27 +- ...ata.EntityFramework.SqlServer.Net10.csproj | 8 +- ...Data.EntityFramework.SqlServer.Net6.csproj | 56 ---- ...Data.EntityFramework.SqlServer.Net7.csproj | 55 ---- ...Data.EntityFramework.SqlServer.Net8.csproj | 8 +- ...Data.EntityFramework.SqlServer.Net9.csproj | 55 ---- ...Core.Data.EntityFramework.SqlServer.csproj | 135 -------- ...tic.Core.Data.EntityFramework.Net10.csproj | 11 +- ...ntic.Core.Data.EntityFramework.Net6.csproj | 57 ---- ...ntic.Core.Data.EntityFramework.Net7.csproj | 57 ---- ...ntic.Core.Data.EntityFramework.Net8.csproj | 11 +- ...ntic.Core.Data.EntityFramework.Net9.csproj | 57 ---- .../eQuantic.Core.Data.EntityFramework.csproj | 53 +--- 25 files changed, 112 insertions(+), 1159 deletions(-) rename src/eQuantic.Core.Data.EntityFramework.MongoDb/{eQuantic.Core.Data.EntityFramework.MongoDb.csproj => eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj} (94%) delete mode 100644 src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj delete mode 100644 src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj diff --git a/eQuantic.Core.Data.EntityFramework.sln b/eQuantic.Core.Data.EntityFramework.sln index 4895d72..0f2964d 100644 --- a/eQuantic.Core.Data.EntityFramework.sln +++ b/eQuantic.Core.Data.EntityFramework.sln @@ -14,52 +14,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "eQuantic.Core.Data.EntityFramework", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.csproj", "{CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net6", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net6.csproj", "{20175F0B-5566-4213-A60E-435A9458B018}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net7", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net7.csproj", "{BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net8", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net8.csproj", "{AB04C373-0DBD-4B6F-811D-D668851BBAD5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.csproj", "{91233051-EE7E-4CBB-8FFD-B900ABBACBBE}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SqlServer", "SqlServer", "{5B80C186-5E75-413B-B825-0B5B2FE9A3E4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MongoDb", "MongoDb", "{F613B6F2-C420-4E72-981A-47A20A538BC6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MongoDb", "src\eQuantic.Core.Data.EntityFramework.MongoDb\eQuantic.Core.Data.EntityFramework.MongoDb.csproj", "{B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net6", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj", "{CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net7", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj", "{068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net8", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj", "{A5610597-89E9-4BE1-8B87-31B7B7E11473}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MongoDb.Net9", "src\eQuantic.Core.Data.EntityFramework.MongoDb\eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj", "{730DEB2B-3B8C-49A4-BD2D-12A274C8790A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net9", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj", "{8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net9", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net9.csproj", "{99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MySql", "MySql", "{9D499CE2-6681-4F5D-8F47-F634EFF79132}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MySql", "src\eQuantic.Core.Data.EntityFramework.MySql\eQuantic.Core.Data.EntityFramework.MySql.csproj", "{7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MySql.Net8", "src\eQuantic.Core.Data.EntityFramework.MySql\eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj", "{C7367715-9273-460F-BE2C-1BB06095E45C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MySql.Net9", "src\eQuantic.Core.Data.EntityFramework.MySql\eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj", "{CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PostgreSql", "PostgreSql", "{A6FAA830-E2CB-48CD-91C6-E0D75BFD8291}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.PostgreSql", "src\eQuantic.Core.Data.EntityFramework.PostgreSql\eQuantic.Core.Data.EntityFramework.PostgreSql.csproj", "{B67128D8-373F-4215-9DCE-85594BEAC0E2}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.PostgreSql.Net8", "src\eQuantic.Core.Data.EntityFramework.PostgreSql\eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj", "{46781B67-3B50-4DC9-BA64-6713528F605A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.PostgreSql.Net9", "src\eQuantic.Core.Data.EntityFramework.PostgreSql\eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj", "{715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net10", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net10.csproj", "{7D85EC36-84C6-4429-BE02-291F046BACA4}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net10", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj", "{D40D237B-5C44-4271-9DCC-53E07DE4342F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.PostgreSql.Net10", "src\eQuantic.Core.Data.EntityFramework.PostgreSql\eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj", "{9BB6467D-1212-4762-96C8-0C1B6E10109F}" @@ -78,6 +46,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Relational", "src\eQuantic.Core.Data.EntityFramework.Relational\eQuantic.Core.Data.EntityFramework.Relational.csproj", "{A6953967-90B5-485D-956E-A62EFE67423F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eQuantic.Core.Data.EntityFramework.MongoDb", "eQuantic.Core.Data.EntityFramework.MongoDb", "{6A49CAD3-8848-795E-F57A-8F1C385ECFED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MongoDb.Net8", "src\eQuantic.Core.Data.EntityFramework.MongoDb\eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj", "{2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eQuantic.Core.Data.EntityFramework", "eQuantic.Core.Data.EntityFramework", "{BEB4D7A8-0559-D436-17A0-F8FA25A80731}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net8", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net8.csproj", "{0CAE00C1-4547-4DDA-A526-A4BBB78898A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net10", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net10.csproj", "{B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,102 +66,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|x64.ActiveCfg = Debug|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|x64.Build.0 = Debug|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|x86.ActiveCfg = Debug|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|x86.Build.0 = Debug|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|Any CPU.Build.0 = Release|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|x64.ActiveCfg = Release|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|x64.Build.0 = Release|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|x86.ActiveCfg = Release|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|x86.Build.0 = Release|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Debug|x64.ActiveCfg = Debug|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Debug|x64.Build.0 = Debug|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Debug|x86.ActiveCfg = Debug|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Debug|x86.Build.0 = Debug|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Release|Any CPU.Build.0 = Release|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Release|x64.ActiveCfg = Release|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Release|x64.Build.0 = Release|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Release|x86.ActiveCfg = Release|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Release|x86.Build.0 = Release|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|x64.ActiveCfg = Debug|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|x64.Build.0 = Debug|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|x86.ActiveCfg = Debug|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|x86.Build.0 = Debug|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|Any CPU.Build.0 = Release|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|x64.ActiveCfg = Release|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|x64.Build.0 = Release|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|x86.ActiveCfg = Release|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|x86.Build.0 = Release|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|x64.ActiveCfg = Debug|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|x64.Build.0 = Debug|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|x86.ActiveCfg = Debug|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|x86.Build.0 = Debug|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|Any CPU.Build.0 = Release|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|x64.ActiveCfg = Release|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|x64.Build.0 = Release|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|x86.ActiveCfg = Release|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|x86.Build.0 = Release|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|x64.ActiveCfg = Debug|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|x64.Build.0 = Debug|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|x86.ActiveCfg = Debug|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|x86.Build.0 = Debug|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|Any CPU.Build.0 = Release|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|x64.ActiveCfg = Release|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|x64.Build.0 = Release|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|x86.ActiveCfg = Release|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|x86.Build.0 = Release|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|x64.ActiveCfg = Debug|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|x64.Build.0 = Debug|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|x86.ActiveCfg = Debug|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|x86.Build.0 = Debug|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|Any CPU.Build.0 = Release|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|x64.ActiveCfg = Release|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|x64.Build.0 = Release|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|x86.ActiveCfg = Release|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|x86.Build.0 = Release|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|x64.ActiveCfg = Debug|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|x64.Build.0 = Debug|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|x86.ActiveCfg = Debug|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|x86.Build.0 = Debug|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|Any CPU.Build.0 = Release|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|x64.ActiveCfg = Release|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|x64.Build.0 = Release|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|x86.ActiveCfg = Release|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|x86.Build.0 = Release|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|x64.ActiveCfg = Debug|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|x64.Build.0 = Debug|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|x86.ActiveCfg = Debug|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|x86.Build.0 = Debug|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|Any CPU.Build.0 = Release|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|x64.ActiveCfg = Release|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|x64.Build.0 = Release|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|x86.ActiveCfg = Release|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|x86.Build.0 = Release|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -196,54 +78,6 @@ Global {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x64.Build.0 = Release|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x86.ActiveCfg = Release|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x86.Build.0 = Release|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|x64.ActiveCfg = Debug|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|x64.Build.0 = Debug|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|x86.ActiveCfg = Debug|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|x86.Build.0 = Debug|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|Any CPU.Build.0 = Release|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|x64.ActiveCfg = Release|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|x64.Build.0 = Release|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|x86.ActiveCfg = Release|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|x86.Build.0 = Release|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|x64.ActiveCfg = Debug|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|x64.Build.0 = Debug|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|x86.ActiveCfg = Debug|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|x86.Build.0 = Debug|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|Any CPU.Build.0 = Release|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|x64.ActiveCfg = Release|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|x64.Build.0 = Release|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|x86.ActiveCfg = Release|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|x86.Build.0 = Release|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|x64.ActiveCfg = Debug|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|x64.Build.0 = Debug|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|x86.ActiveCfg = Debug|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|x86.Build.0 = Debug|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|Any CPU.Build.0 = Release|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|x64.ActiveCfg = Release|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|x64.Build.0 = Release|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|x86.ActiveCfg = Release|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|x86.Build.0 = Release|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|x64.ActiveCfg = Debug|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|x64.Build.0 = Debug|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|x86.ActiveCfg = Debug|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|x86.Build.0 = Debug|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|Any CPU.Build.0 = Release|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|x64.ActiveCfg = Release|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|x64.Build.0 = Release|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|x86.ActiveCfg = Release|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|x86.Build.0 = Release|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -256,30 +90,6 @@ Global {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x64.Build.0 = Release|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x86.ActiveCfg = Release|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x86.Build.0 = Release|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|x64.ActiveCfg = Debug|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|x64.Build.0 = Debug|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|x86.ActiveCfg = Debug|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|x86.Build.0 = Debug|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|Any CPU.Build.0 = Release|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|x64.ActiveCfg = Release|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|x64.Build.0 = Release|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|x86.ActiveCfg = Release|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|x86.Build.0 = Release|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|x64.ActiveCfg = Debug|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|x64.Build.0 = Debug|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|x86.ActiveCfg = Debug|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|x86.Build.0 = Debug|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|Any CPU.Build.0 = Release|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|x64.ActiveCfg = Release|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|x64.Build.0 = Release|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|x86.ActiveCfg = Release|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|x86.Build.0 = Release|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|Any CPU.Build.0 = Debug|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -292,30 +102,6 @@ Global {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x64.Build.0 = Release|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x86.ActiveCfg = Release|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x86.Build.0 = Release|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|x64.ActiveCfg = Debug|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|x64.Build.0 = Debug|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|x86.ActiveCfg = Debug|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|x86.Build.0 = Debug|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|Any CPU.Build.0 = Release|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|x64.ActiveCfg = Release|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|x64.Build.0 = Release|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|x86.ActiveCfg = Release|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|x86.Build.0 = Release|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|x64.ActiveCfg = Debug|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|x64.Build.0 = Debug|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|x86.ActiveCfg = Debug|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|x86.Build.0 = Debug|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|Any CPU.Build.0 = Release|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|x64.ActiveCfg = Release|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|x64.Build.0 = Release|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|x86.ActiveCfg = Release|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|x86.Build.0 = Release|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|Any CPU.Build.0 = Debug|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -412,34 +198,54 @@ Global {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x64.Build.0 = Release|Any CPU {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x86.ActiveCfg = Release|Any CPU {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x86.Build.0 = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|x64.ActiveCfg = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|x64.Build.0 = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|x86.ActiveCfg = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|x86.Build.0 = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|Any CPU.Build.0 = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|x64.ActiveCfg = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|x64.Build.0 = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|x86.ActiveCfg = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|x86.Build.0 = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|x64.Build.0 = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|x86.Build.0 = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|Any CPU.Build.0 = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|x64.ActiveCfg = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|x64.Build.0 = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|x86.ActiveCfg = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|x86.Build.0 = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|x64.Build.0 = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|x86.Build.0 = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|Any CPU.Build.0 = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|x64.ActiveCfg = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|x64.Build.0 = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|x86.ActiveCfg = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {20175F0B-5566-4213-A60E-435A9458B018} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {AB04C373-0DBD-4B6F-811D-D668851BBAD5} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {F613B6F2-C420-4E72-981A-47A20A538BC6} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D} = {F613B6F2-C420-4E72-981A-47A20A538BC6} - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} {A5610597-89E9-4BE1-8B87-31B7B7E11473} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A} = {F613B6F2-C420-4E72-981A-47A20A538BC6} - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {9D499CE2-6681-4F5D-8F47-F634EFF79132} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8} = {9D499CE2-6681-4F5D-8F47-F634EFF79132} {C7367715-9273-460F-BE2C-1BB06095E45C} = {9D499CE2-6681-4F5D-8F47-F634EFF79132} - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD} = {9D499CE2-6681-4F5D-8F47-F634EFF79132} {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {B67128D8-373F-4215-9DCE-85594BEAC0E2} = {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} {46781B67-3B50-4DC9-BA64-6713528F605A} = {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6} = {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} - {7D85EC36-84C6-4429-BE02-291F046BACA4} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {D40D237B-5C44-4271-9DCC-53E07DE4342F} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} {9BB6467D-1212-4762-96C8-0C1B6E10109F} = {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748} = {9D499CE2-6681-4F5D-8F47-F634EFF79132} @@ -448,6 +254,11 @@ Global {A730FE2E-2972-41F6-AAF7-3AEE017A7418} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} {A6953967-90B5-485D-956E-A62EFE67423F} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} + {6A49CAD3-8848-795E-F57A-8F1C385ECFED} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94} = {6A49CAD3-8848-795E-F57A-8F1C385ECFED} + {BEB4D7A8-0559-D436-17A0-F8FA25A80731} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2} = {BEB4D7A8-0559-D436-17A0-F8FA25A80731} + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354} = {BEB4D7A8-0559-D436-17A0-F8FA25A80731} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83FECDD1-8A97-40B1-8529-3D5216E674C3} diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj index f753757..dca97c2 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and Mongo DB eQuantic.Core.Data.EntityFramework.MongoDb - 10.0.2.0 + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework.MongoDb eQuantic.Core.Data.EntityFramework.MongoDb @@ -12,8 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 10.0.2.0 - 10.0.2.0 + 10.1.0.0 + 10.1.0.0 enable enable @@ -21,7 +21,7 @@ - + all diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj similarity index 94% rename from src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj rename to src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj index 7a4e63b..c642da5 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and Mongo DB eQuantic.Core.Data.EntityFramework.MongoDb - 8.1.2.0 + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework.MongoDb eQuantic.Core.Data.EntityFramework.MongoDb @@ -12,8 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 8.1.2.0 - 8.1.2.0 + 8.2.0.0 + 8.2.0.0 enable enable @@ -21,7 +21,7 @@ - + all diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj deleted file mode 100644 index 2284b3d..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj +++ /dev/null @@ -1,54 +0,0 @@ - - - - - Core Data library for Entity Framework and Mongo DB - eQuantic.Core.Data.EntityFramework.MongoDb - 9.1.2.0 - net9.0 - eQuantic.Core.Data.EntityFramework.MongoDb - eQuantic.Core.Data.EntityFramework.MongoDb - eQuantic;Core;Data;Library;Repository;Pattern;MongoDB - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 9.1.2.0 - 9.1.2.0 - - enable - enable - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj index c5a9c99..fd1c94b 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and MySQL eQuantic.Core.Data.EntityFramework.MySql - 10.0.2.0 + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework.MySql eQuantic.Core.Data.EntityFramework.MySql @@ -12,13 +12,13 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 10.0.2.0 - 10.0.2.0 + 10.1.0.0 + 10.1.0.0 - + all diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj index 2ed130f..9e3e06b 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and MySQL eQuantic.Core.Data.EntityFramework.MySql - 8.1.2.0 + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework.MySql eQuantic.Core.Data.EntityFramework.MySql @@ -12,13 +12,13 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 8.1.2.0 - 8.1.2.0 + 8.2.0.0 + 8.2.0.0 - + all diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj deleted file mode 100644 index 47d644d..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - - Core Data library for Entity Framework and MySQL - eQuantic.Core.Data.EntityFramework.MySql - 9.1.2.0 - net9.0 - eQuantic.Core.Data.EntityFramework.MySql - eQuantic.Core.Data.EntityFramework.MySql - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 9.1.2.0 - 9.1.2.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj deleted file mode 100644 index c3588ed..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj +++ /dev/null @@ -1,90 +0,0 @@ - - - - - Core Data library for Entity Framework and MySQL - eQuantic.Core.Data.EntityFramework.MySql - 4.4.2.0 - net8.0;net9.0;net10.0 - eQuantic.Core.Data.EntityFramework.MySql - eQuantic.Core.Data.EntityFramework.MySql - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 4.4.2.0 - 4.4.2.0 - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj index 80b667a..d1680a7 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and PostgreSQL eQuantic.Core.Data.EntityFramework.PostgreSql - 10.0.2.0 + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework.PostgreSql eQuantic.Core.Data.EntityFramework.PostgreSql @@ -12,13 +12,13 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 10.0.2.0 - 10.0.2.0 + 10.1.0.0 + 10.1.0.0 - + all diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj index 1a6183a..244d5db 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and PostgreSQL eQuantic.Core.Data.EntityFramework.PostgreSql - 8.1.2.0 + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework.PostgreSql eQuantic.Core.Data.EntityFramework.PostgreSql @@ -12,13 +12,13 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 8.1.2.0 - 8.1.2.0 + 8.2.0.0 + 8.2.0.0 - + all diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj deleted file mode 100644 index faaad6e..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - - Core Data library for Entity Framework and PostgreSQL - eQuantic.Core.Data.EntityFramework.PostgreSql - 9.1.2.0 - net9.0 - eQuantic.Core.Data.EntityFramework.PostgreSql - eQuantic.Core.Data.EntityFramework.PostgreSql - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 9.1.2.0 - 9.1.2.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj deleted file mode 100644 index 7a96e47..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj +++ /dev/null @@ -1,88 +0,0 @@ - - - - - Core Data library for Entity Framework and PostgreSQL - eQuantic.Core.Data.EntityFramework.PostgreSql - 4.4.2.0 - net8.0;net9.0;net10.0 - eQuantic.Core.Data.EntityFramework.PostgreSql - eQuantic.Core.Data.EntityFramework.PostgreSql - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 4.4.2.0 - 4.4.2.0 - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj index 7af18a1..d4e9992 100644 --- a/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj @@ -4,39 +4,26 @@ Shared relational implementation for eQuantic Core Data Entity Framework providers eQuantic.Core.Data.EntityFramework.Relational - 1.0.0.0 - net6.0;net7.0;net8.0;net9.0;net10.0 + 4.0.0.0 + net8.0;net10.0 eQuantic.Core.Data.EntityFramework.Relational eQuantic.Core.Data.EntityFramework.Relational eQuantic;Core;Data;Library;Repository;Pattern;SQL;Relational Shared relational base used by the SqlServer, PostgreSql and MySql providers - 1.0.0.0 - 1.0.0.0 + 4.0.0.0 + 4.0.0.0 - - - - - - - - - - + - - - - @@ -60,8 +47,8 @@ + - + diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj index 312361a..d1cb529 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer - 10.0.2.0 + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -12,13 +12,13 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 10.0.2.0 - 10.0.2.0 + 10.1.0.0 + 10.1.0.0 - + all diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj deleted file mode 100644 index e0d2206..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj +++ /dev/null @@ -1,56 +0,0 @@ - - - - - Core Data library for Entity Framework and SQL Server - eQuantic.Core.Data.EntityFramework.SqlServer - 6.1.2.0 - net6.0 - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 6.1.2.0 - 6.1.2.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj deleted file mode 100644 index 4cfbda9..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - - Core Data library for Entity Framework and SQL Server - eQuantic.Core.Data.EntityFramework.SqlServer - 7.1.2.0 - net7.0 - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 7.1.2.0 - 7.1.2.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj index 16b5a0b..693acad 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer - 8.1.2.0 + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -12,13 +12,13 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 8.1.2.0 - 8.1.2.0 + 8.2.0.0 + 8.2.0.0 - + all diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj deleted file mode 100644 index 887c7c4..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - - Core Data library for Entity Framework and SQL Server - eQuantic.Core.Data.EntityFramework.SqlServer - 9.1.2.0 - net9.0 - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 9.1.2.0 - 9.1.2.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj deleted file mode 100644 index 5515144..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj +++ /dev/null @@ -1,135 +0,0 @@ - - - - - Core Data library for Entity Framework and SQL Server - eQuantic.Core.Data.EntityFramework.SqlServer - 4.4.2.0 - net6.0;net7.0;net8.0;net9.0;net10.0 - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 4.4.2.0 - 4.4.2.0 - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj index 467df6b..7de55e9 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework - 10.0.2.0 + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -12,13 +12,13 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 10.0.2.0 - 10.0.2.0 + 10.1.0.0 + 10.1.0.0 - + all @@ -53,5 +53,8 @@ <_Parameter1>$(AssemblyName).MongoDb + + <_Parameter1>$(AssemblyName).Relational + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj deleted file mode 100644 index 3114d85..0000000 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj +++ /dev/null @@ -1,57 +0,0 @@ - - - - - Core Data library for Entity Framework - eQuantic.Core.Data.EntityFramework - 6.1.2.0 - net6.0 - eQuantic.Core.Data.EntityFramework - eQuantic.Core.Data.EntityFramework - eQuantic;Core;Data;Library;Repository;Pattern - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 6.1.2.0 - 6.1.2.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - <_Parameter1>$(AssemblyName).SqlServer - - - <_Parameter1>$(AssemblyName).PostgreSql - - - <_Parameter1>$(AssemblyName).MySql - - - <_Parameter1>$(AssemblyName).MongoDb - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj deleted file mode 100644 index 95c6ca2..0000000 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj +++ /dev/null @@ -1,57 +0,0 @@ - - - - - Core Data library for Entity Framework - eQuantic.Core.Data.EntityFramework - 7.1.2.0 - net7.0 - eQuantic.Core.Data.EntityFramework - eQuantic.Core.Data.EntityFramework - eQuantic;Core;Data;Library;Repository;Pattern - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 7.1.2.0 - 7.1.2.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - <_Parameter1>$(AssemblyName).SqlServer - - - <_Parameter1>$(AssemblyName).PostgreSql - - - <_Parameter1>$(AssemblyName).MySql - - - <_Parameter1>$(AssemblyName).MongoDb - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj index 13675ee..2fd1bcd 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj @@ -4,7 +4,7 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework - 8.1.2.0 + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -12,13 +12,13 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - 8.1.2.0 - 8.1.2.0 + 8.2.0.0 + 8.2.0.0 - + all @@ -53,5 +53,8 @@ <_Parameter1>$(AssemblyName).MongoDb + + <_Parameter1>$(AssemblyName).Relational + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj deleted file mode 100644 index e5660aa..0000000 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj +++ /dev/null @@ -1,57 +0,0 @@ - - - - - Core Data library for Entity Framework - eQuantic.Core.Data.EntityFramework - 9.1.2.0 - net9.0 - eQuantic.Core.Data.EntityFramework - eQuantic.Core.Data.EntityFramework - eQuantic;Core;Data;Library;Repository;Pattern - Entity ignorant persistance with Repository Pattern for Entity - Framework - - 9.1.2.0 - 9.1.2.0 - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - <_Parameter1>$(AssemblyName).SqlServer - - - <_Parameter1>$(AssemblyName).PostgreSql - - - <_Parameter1>$(AssemblyName).MySql - - - <_Parameter1>$(AssemblyName).MongoDb - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj index 508f0a8..04b3636 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj @@ -1,48 +1,23 @@ - + Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework - 4.4.2.0 - net6.0;net7.0;net8.0;net9.0;net10.0 + 4.5.0.0 + net8.0;net10.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework eQuantic;Core;Data;Library;Repository;Pattern - Entity ignorant persistance with Repository Pattern for Entity - Framework + Entity ignorant persistance with Repository Pattern for Entity Framework - 4.4.2.0 - 4.4.2.0 + 4.5.0.0 + 4.5.0.0 - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -57,18 +32,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - @@ -108,4 +71,4 @@ <_Parameter1>$(AssemblyName).MongoDb - \ No newline at end of file + From 4f2fd90492810a692b6a5f7dae7bd695449350f2 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 15:42:56 +0100 Subject: [PATCH 22/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(core):=20mi?= =?UTF-8?q?grate=20base=20repositories=20to=20eQuantic.Core.Data=20v5=20co?= =?UTF-8?q?ntracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/AsyncQueryableRepository.cs | 723 +-------------- .../Extensions/QueryableExtensions.cs | 131 ++- .../Extensions/ServiceCollectionExtensions.cs | 38 +- .../Repository/QueryableRepository.cs | 343 +------- .../Read/AsyncQueryableReadRepository.cs | 820 +++--------------- .../Read/QueryableReadRepository.cs | 419 +++------ .../Repository/SetBase.cs | 45 +- .../Repository/Write/AsyncWriteRepository.cs | 27 +- .../Repository/Write/WriteRepository.cs | 51 +- 9 files changed, 502 insertions(+), 2095 deletions(-) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs index b33fe2a..dd92dd8 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; @@ -7,767 +7,134 @@ using eQuantic.Core.Data.EntityFramework.Repository.Read; using eQuantic.Core.Data.EntityFramework.Repository.Write; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Read; -using eQuantic.Core.Data.Repository.Write; using eQuantic.Linq.Specification; namespace eQuantic.Core.Data.EntityFramework.Repository; [ExcludeFromCodeCoverage] -public class AsyncQueryableRepository : - QueryableRepository, - IAsyncRepository, TEntity, TKey>, - IAsyncQueryableRepository - where TUnitOfWork : class, IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class AsyncQueryableRepository : + AsyncQueryableReadRepository, + IAsyncQueryableRepository, + IQueryableRepository + where TEntity : class, IEntity { - private readonly IAsyncQueryableReadRepository _asyncReadRepository; - private readonly IAsyncWriteRepository _asyncWriteRepository; + private readonly AsyncWriteRepository _asyncWriteRepository; - public AsyncQueryableRepository(TUnitOfWork unitOfWork) : base(unitOfWork) + public AsyncQueryableRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { - var asyncReadRepository = new AsyncQueryableReadRepository(unitOfWork); - asyncReadRepository.OwnUnitOfWork = false; - this._asyncReadRepository = asyncReadRepository; - - var asyncWriteRepository = new AsyncWriteRepository(unitOfWork); - asyncWriteRepository.OwnUnitOfWork = false; - this._asyncWriteRepository = asyncWriteRepository; + _asyncWriteRepository = new AsyncWriteRepository(unitOfWork); } - public Task AddAsync(TEntity item) - { - return this._asyncWriteRepository.AddAsync(item); - } - - public Task> AllMatchingAsync( - ISpecification specification, - Action> configuration = default) - { - return this._asyncReadRepository.AllMatchingAsync(specification, configuration); - } - - public Task> AllMatchingAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllMatchingAsync(specification, configuration, cancellationToken); - } - public Task> AllMatchingAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllMatchingAsync(specification, cancellationToken); - } - - public Task CountAsync(CancellationToken cancellationToken = default) - { - return this._asyncReadRepository.CountAsync(cancellationToken); - } - - public Task CountAsync( - ISpecification specification, - CancellationToken cancellationToken = default) - { - return this._asyncReadRepository.CountAsync(specification, cancellationToken); - } - - public Task CountAsync( - Expression> filter, - CancellationToken cancellationToken = default) - { - return this._asyncReadRepository.CountAsync(filter, cancellationToken); - } - - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); - } + // -------------------------------------------------------------- synchronous write (IWriteRepository) - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) + public void Add(TEntity item) { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.Add(item); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) + public void AddRange(IEnumerable items) { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.AddRange(items); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) + public long DeleteMany(Expression> filter) { - return _asyncReadRepository.SumAsync(filter, source); + return _asyncWriteRepository.DeleteMany(filter); } - public Task SumAsync(Expression> source) + public long DeleteMany(ISpecification specification) { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + return _asyncWriteRepository.DeleteMany(specification); } - public Task SumAsync(Expression> source) + public void Merge(TEntity persisted, TEntity current) { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.Merge(persisted, current); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) + public void Modify(TEntity item) { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.Modify(item); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) + public void Remove(TEntity item) { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.Remove(item); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) + public void TrackItem(TEntity item) { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.TrackItem(item); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); - } - - public Task AllAsync( - ISpecification specification, - Action> configuration = default) + public long UpdateMany(Expression> filter, Expression> updateFactory) { - return this._asyncReadRepository.AllAsync(specification, configuration); - } - - public Task AllAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllAsync(specification, configuration, cancellationToken); - } - - public Task AllAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllAsync(specification, cancellationToken); + return _asyncWriteRepository.UpdateMany(filter, updateFactory); } - public Task AllAsync( - Expression> filter, - Action> configuration = default) + public long UpdateMany(ISpecification specification, Expression> updateFactory) { - return this._asyncReadRepository.AllAsync(filter, configuration); - } - - public Task AllAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllAsync(filter, configuration, cancellationToken); - } - - public Task AllAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllAsync(filter, cancellationToken); + return _asyncWriteRepository.UpdateMany(specification, updateFactory); } - public Task AnyAsync( - Action> configuration = default, - CancellationToken cancellationToken = default) - { - return this._asyncReadRepository.AnyAsync(configuration, cancellationToken); - } + // -------------------------------------------------------------- asynchronous write (IAsyncWriteRepository) - public Task AnyAsync( - ISpecification specification, - Action> configuration = default) - { - return this._asyncReadRepository.AnyAsync(specification, configuration); - } - - public Task AnyAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) + public Task AddAsync(TEntity item, CancellationToken cancellationToken = default) { - return this._asyncReadRepository.AnyAsync(specification, configuration, cancellationToken); - } - - public Task AnyAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AnyAsync(specification, cancellationToken); + return _asyncWriteRepository.AddAsync(item, cancellationToken); } - public Task AnyAsync( - Expression> filter, - Action> configuration = default) - { - return this._asyncReadRepository.AnyAsync(filter, configuration); - } - - public Task AnyAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) + public Task AddRangeAsync(IEnumerable items, CancellationToken cancellationToken = default) { - return this._asyncReadRepository.AnyAsync(filter, configuration, cancellationToken); - } - - public Task AnyAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AnyAsync(filter, cancellationToken); + return _asyncWriteRepository.AddRangeAsync(items, cancellationToken); } - public Task DeleteManyAsync( - Expression> filter, + public Task DeleteManyAsync(Expression> filter, CancellationToken cancellationToken = default) { - return this._asyncWriteRepository.DeleteManyAsync(filter, cancellationToken); + return _asyncWriteRepository.DeleteManyAsync(filter, cancellationToken); } - public Task DeleteManyAsync( - ISpecification specification, + public Task DeleteManyAsync(ISpecification specification, CancellationToken cancellationToken = default) { - return this._asyncWriteRepository.DeleteManyAsync(specification, cancellationToken); - } - - public Task> GetAllAsync( - Action> configuration = default) - { - return this._asyncReadRepository.GetAllAsync(configuration); - } - - public Task> GetAllAsync( - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetAllAsync(configuration, cancellationToken); - } - - public Task> GetAllAsync( - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetAllAsync(cancellationToken); - } - - public Task GetAsync( - TKey id, - Action> configuration = default) - { - return this._asyncReadRepository.GetAsync(id, configuration); - } - - public Task GetAsync( - TKey id, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetAsync(id, configuration, cancellationToken); - } - - public Task GetAsync( - TKey id, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetAsync(id, cancellationToken); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - Action> configuration = default) - { - return this._asyncReadRepository.GetMappedAsync(filter, map, configuration); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetMappedAsync(filter, map, configuration, cancellationToken); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetMappedAsync(filter, map, cancellationToken); - } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration = default) - { - return this._asyncReadRepository.GetMappedAsync(specification, map, configuration); - } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetMappedAsync(specification, map, configuration, cancellationToken); - } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetMappedAsync(specification, map, cancellationToken); - } - - public Task> GetFilteredAsync( - Expression> filter, - Action> configuration = default) - { - return this._asyncReadRepository.GetFilteredAsync(filter, configuration); - } - - public Task> GetFilteredAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFilteredAsync(filter, configuration, cancellationToken); - } - - public Task> GetFilteredAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFilteredAsync(filter, cancellationToken); - } - - public Task GetFirstAsync( - Expression> filter, - Action> configuration = default) - { - return this._asyncReadRepository.GetFirstAsync(filter, configuration); - } - - public Task GetFirstAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstAsync(filter, configuration, cancellationToken); - } - - public Task GetFirstAsync( - Expression> filter, - CancellationToken cancellationToken = default) - { - return this._asyncReadRepository.GetFirstAsync(filter, cancellationToken); - } - - public Task GetFirstAsync( - ISpecification specification, - Action> configuration = default) - { - return this._asyncReadRepository.GetFirstAsync(specification, configuration); - } - - public Task GetFirstAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstAsync(specification, configuration, cancellationToken); - } - - public Task GetFirstAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstAsync(specification, cancellationToken); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - Action> configuration = default) - { - return this._asyncReadRepository.GetFirstMappedAsync(filter, map, configuration); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstMappedAsync(filter, map, configuration, cancellationToken); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstMappedAsync(filter, map, cancellationToken); - } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration = default) - { - return this._asyncReadRepository.GetFirstMappedAsync(specification, map, configuration); - } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstMappedAsync(specification, map, configuration, cancellationToken); - } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstMappedAsync(specification, map, cancellationToken); - } - - public Task> GetPagedAsync( - int limit, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(limit, configuration); - } - - public Task> GetPagedAsync( - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - int limit, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(limit, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(specification, limit, configuration); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(specification, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(specification, limit, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(filter, limit, configuration); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(filter, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(filter, limit, cancellationToken); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(pageIndex, pageSize, configuration); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(pageIndex, pageSize, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(pageIndex, pageSize, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(specification, pageIndex, pageSize, configuration); - } - - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(specification, pageIndex, pageSize, configuration, - cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(specification, pageIndex, pageSize, - cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(filter, pageIndex, pageSize, configuration); - } - - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(filter, pageIndex, pageSize, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(filter, pageIndex, pageSize, cancellationToken); - } - - public Task GetSingleAsync( - Expression> filter, - Action> configuration = default) - { - return this._asyncReadRepository.GetSingleAsync(filter, configuration); - } - - public Task GetSingleAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetSingleAsync(filter, configuration, cancellationToken); - } - - public Task GetSingleAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetSingleAsync(filter, cancellationToken); - } - - public Task GetSingleAsync( - ISpecification specification, - Action> configuration = default) - { - return this._asyncReadRepository.GetSingleAsync(specification, configuration); - } - - public Task GetSingleAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetSingleAsync(specification, configuration, cancellationToken); - } - - public Task GetSingleAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetSingleAsync(specification, cancellationToken); + return _asyncWriteRepository.DeleteManyAsync(specification, cancellationToken); } public Task MergeAsync(TEntity persisted, TEntity current) { - return this._asyncWriteRepository.MergeAsync(persisted, current); + return _asyncWriteRepository.MergeAsync(persisted, current); } public Task ModifyAsync(TEntity item) { - return this._asyncWriteRepository.ModifyAsync(item); + return _asyncWriteRepository.ModifyAsync(item); } public Task RemoveAsync(TEntity item) { - return this._asyncWriteRepository.RemoveAsync(item); + return _asyncWriteRepository.RemoveAsync(item); } public Task UpdateManyAsync(Expression> filter, Expression> updateFactory, CancellationToken cancellationToken = default) { - return this._asyncWriteRepository.UpdateManyAsync(filter, updateFactory, cancellationToken); + return _asyncWriteRepository.UpdateManyAsync(filter, updateFactory, cancellationToken); } public Task UpdateManyAsync(ISpecification specification, Expression> updateFactory, CancellationToken cancellationToken = default) { - return this._asyncWriteRepository.UpdateManyAsync(specification, updateFactory, cancellationToken); + return _asyncWriteRepository.UpdateManyAsync(specification, updateFactory, cancellationToken); } protected override void Dispose(bool disposing) { - if (Disposed) - { - return; - } - if (disposing) { - this._asyncReadRepository?.Dispose(); - this._asyncWriteRepository?.Dispose(); + _asyncWriteRepository?.Dispose(); } - // Base disposes the sync sub-repositories and the unit of work, and flips the shared - // Disposed flag. The unit of work is therefore disposed exactly once. base.Dispose(disposing); } } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/QueryableExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/QueryableExtensions.cs index d90f8da..ec918c4 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/QueryableExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/QueryableExtensions.cs @@ -1,6 +1,9 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using eQuantic.Core.Data.Repository.Options; +using eQuantic.Linq.Web; using Microsoft.EntityFrameworkCore; namespace eQuantic.Core.Data.EntityFramework.Repository.Extensions; @@ -48,4 +51,130 @@ public static IQueryable IncludeMany(this IQueryable return query; } + + /// + /// Applies the ordered set of sortings to the query, translating each + /// into the matching OrderBy/OrderByDescending/ThenBy/ + /// ThenByDescending call so the ordering stays server-side (EF translatable). + /// + /// The type of the entity. + /// The query. + /// The sortings, in application order. + /// The ordered query. + public static IQueryable ApplySorts(this IQueryable query, + IReadOnlyList> sortings) + { + if (sortings is not { Count: > 0 }) + { + return query; + } + + var first = true; + foreach (var sort in sortings) + { + if (sort == null) + { + continue; + } + + var method = first + ? (sort.Direction == SortDirection.Ascending ? nameof(Queryable.OrderBy) : nameof(Queryable.OrderByDescending)) + : (sort.Direction == SortDirection.Ascending ? nameof(Queryable.ThenBy) : nameof(Queryable.ThenByDescending)); + + var call = Expression.Call( + typeof(Queryable), + method, + new[] { typeof(TEntity), sort.KeySelector.ReturnType }, + query.Expression, + Expression.Quote(sort.KeySelector)); + + query = query.Provider.CreateQuery(call); + first = false; + } + + return query; + } + + /// + /// Translates a into an , + /// applying, in order: → + /// and + /// → the per-call → eager + /// → + /// → → + /// → + /// → . + /// + /// The type of the entity. + /// The source query (typically the entity set). + /// The query options, or null when no shaping is requested. + /// + /// An optional per-call transformation (e.g. an explicit filter) applied right after the options' + /// own filter and before eager loading, sorting and the remaining shaping. + /// + /// The shaped query. + internal static IQueryable ApplyOptions(this IQueryable source, + QueryOptions options, + Func, IQueryable> internalQueryAction = null) + where TEntity : class + { + var query = source; + + if (options?.BeforeCustomization != null) + { + query = options.BeforeCustomization(query); + } + + if (options?.Specification != null) + { + query = query.Where(options.Specification.SatisfiedBy()); + } + + if (options?.Filter != null) + { + query = query.Where(options.Filter); + } + + if (internalQueryAction != null) + { + query = internalQueryAction(query); + } + + if (options == null) + { + return query; + } + + if (options.IncludePaths.Count > 0) + { + query = query.IncludeMany(options.IncludePaths.ToArray()); + } + + if (options.Sortings.Count > 0) + { + query = query.ApplySorts(options.Sortings); + } + + if (options.AsNoTracking) + { + query = query.AsNoTracking(); + } + + if (options.IgnoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (!string.IsNullOrEmpty(options.Tag)) + { + query = query.TagWith(options.Tag); + } + + if (options.AfterCustomization != null) + { + query = options.AfterCustomization(query); + } + + return query; + } } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs index fc1ae46..4f380cc 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using eQuantic.Core.Data.EntityFramework.Repository.Options; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Sql; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -33,7 +32,7 @@ public static IServiceCollection AddQueryableRepositories(services, lifetime); AddGenericRepositories(services, lifetime); @@ -46,7 +45,7 @@ public static IServiceCollection AddQueryableRepositories( { var repoOptions = GetOptions(options); var lifetime = repoOptions.GetLifetime(); - + AddUnitOfWork(services, lifetime); AddGenericRepositories(services, lifetime); @@ -59,7 +58,7 @@ public static IServiceCollection AddCustomRepositories(thi { var repoOptions = GetOptions(options); var lifetime = repoOptions.GetLifetime(); - + AddUnitOfWork(services, lifetime); AddRepositories(services, repoOptions); @@ -72,22 +71,14 @@ private static void AddUnitOfWork(IServic { services.TryAdd(new ServiceDescriptor(typeof(TUnitOfWorkInterface), typeof(TUnitOfWorkImpl), lifetime)); services.TryAdd(new ServiceDescriptor(typeof(IQueryableUnitOfWork), sp => sp.GetRequiredService(), lifetime)); - - // Only expose ISqlUnitOfWork when the implementation actually provides it. Non-relational - // unit of works (e.g. MongoDb) implement IQueryableUnitOfWork but not ISqlUnitOfWork; - // registering it unconditionally made resolving ISqlUnitOfWork throw InvalidCastException. - if (typeof(ISqlUnitOfWork).IsAssignableFrom(typeof(TUnitOfWorkImpl))) - { - services.TryAdd(new ServiceDescriptor(typeof(ISqlUnitOfWork), sp => sp.GetRequiredService(), lifetime)); - } } private static void AddGenericRepositories(IServiceCollection services, ServiceLifetime lifetime) { - services.TryAdd(new ServiceDescriptor(typeof(IQueryableRepository<,,>), typeof(QueryableRepository<,,>), lifetime)); - services.TryAdd(new ServiceDescriptor(typeof(IAsyncQueryableRepository<,,>), typeof(AsyncQueryableRepository<,,>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IQueryableRepository<,>), typeof(QueryableRepository<,>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IAsyncQueryableRepository<,>), typeof(AsyncQueryableRepository<,>), lifetime)); } - + private static void AddRepositories(IServiceCollection services, RepositoryOptions repoOptions) { var lifetime = repoOptions.GetLifetime(); @@ -97,10 +88,10 @@ private static void AddRepositories(IServiceCollection services, RepositoryOptio o.GetInterfaces().Any(i => i == typeof(IRepository))); foreach (var type in types) { - AddRepository(typeof(IRepository<,,>), type, services, lifetime); - AddRepository(typeof(IAsyncRepository<,,>), type, services, lifetime); - AddRepository(typeof(IQueryableRepository<,,>), type, services, lifetime); - AddRepository(typeof(IAsyncQueryableRepository<,,>), type, services, lifetime); + AddRepository(typeof(IRepository<,>), type, services, lifetime); + AddRepository(typeof(IAsyncRepository<,>), type, services, lifetime); + AddRepository(typeof(IQueryableRepository<,>), type, services, lifetime); + AddRepository(typeof(IAsyncQueryableRepository<,>), type, services, lifetime); } } @@ -115,14 +106,13 @@ private static void AddRepository(Type interfaceType, Type type, IServiceCollect return; } - var uowType = repoInterface.GenericTypeArguments[0]; - var entityType = repoInterface.GenericTypeArguments[1]; - var keyType = repoInterface.GenericTypeArguments[2]; + var entityType = repoInterface.GenericTypeArguments[0]; + var keyType = repoInterface.GenericTypeArguments[1]; // Honour the configured lifetime instead of forcing Transient, and use TryAdd so calling the // registration twice does not produce duplicate descriptors. services.TryAdd(new ServiceDescriptor( - interfaceType.MakeGenericType(uowType, entityType, keyType), type, lifetime)); + interfaceType.MakeGenericType(entityType, keyType), type, lifetime)); } private static IEnumerable GetLoadableTypes(Assembly assembly) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs index 287b970..a05eb4a 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs @@ -1,388 +1,89 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using eQuantic.Core.Data.EntityFramework.Repository.Read; using eQuantic.Core.Data.EntityFramework.Repository.Write; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Read; using eQuantic.Core.Data.Repository.Write; using eQuantic.Linq.Specification; namespace eQuantic.Core.Data.EntityFramework.Repository; [ExcludeFromCodeCoverage] -public class QueryableRepository : - IRepository, TEntity, TKey>, - IQueryableRepository - where TUnitOfWork : IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class QueryableRepository : + QueryableReadRepository, + IQueryableRepository + where TEntity : class, IEntity { - private readonly IQueryableReadRepository _readRepository; - private readonly IWriteRepository _writeRepository; - - /// - /// Shared disposal flag. Kept protected so derived repositories observe the same state - /// instead of shadowing it — shadowing let both levels run their disposal block and dispose the - /// unit of work twice. - /// - protected bool Disposed; + private readonly IWriteRepository _writeRepository; /// /// Create a new instance of repository /// /// Associated Unit Of Work - public QueryableRepository(TUnitOfWork unitOfWork) + public QueryableRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { - this.UnitOfWork = unitOfWork; - var readRepository = new QueryableReadRepository(unitOfWork); - readRepository.OwnUnitOfWork = false; - this._readRepository = readRepository; - - var writeRepository = new WriteRepository(unitOfWork); - writeRepository.OwnUnitOfWork = false; - this._writeRepository = writeRepository; + _writeRepository = new WriteRepository(unitOfWork); } - public TUnitOfWork UnitOfWork { get; private set; } - public void Add(TEntity item) { - this._writeRepository.Add(item); - } - - public IEnumerable AllMatching(ISpecification specification, - Action> configuration = default) - { - return this._readRepository.AllMatching(specification, configuration); - } - - public long Count() - { - return this._readRepository.Count(); - } - - public long Count(ISpecification specification) - { - return this._readRepository.Count(specification); - } - - public long Count(Expression> filter) - { - return this._readRepository.Count(filter); - } - - public int Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public int Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public int Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public int? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public int? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public int? Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public long Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public long Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public long Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public long? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public long? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public long? Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public double Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public double Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public double Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public double? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public double? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public double? Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public float Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public float Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public float Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public float? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public float? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public float? Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public decimal Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public decimal Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public decimal Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public decimal? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public decimal? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public decimal? Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public bool All(ISpecification specification, Action> configuration = default) - { - return this._readRepository.All(specification, configuration); - } - - public bool All(Expression> filter, Action> configuration = default) - { - return this._readRepository.All(filter, configuration); - } - - public bool Any(Action> configuration = default) - { - return this._readRepository.Any(configuration); - } - - public bool Any(ISpecification specification, Action> configuration = default) - { - return this._readRepository.Any(specification, configuration); + _writeRepository.Add(item); } - public bool Any(Expression> filter, Action> configuration = default) + public void AddRange(IEnumerable items) { - return this._readRepository.Any(filter, configuration); + _writeRepository.AddRange(items); } public long DeleteMany(Expression> filter) { - return this._writeRepository.DeleteMany(filter); + return _writeRepository.DeleteMany(filter); } public long DeleteMany(ISpecification specification) { - return this._writeRepository.DeleteMany(specification); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public TEntity Get(TKey id, Action> configuration = default) - { - return this._readRepository.Get(id, configuration); - } - - public IEnumerable GetAll(Action> configuration = default) - { - return this._readRepository.GetAll(configuration); - } - - public IEnumerable GetMapped(Expression> filter, - Expression> map, Action> configuration = default) - { - return this._readRepository.GetMapped(filter, map, configuration); - } - - public IEnumerable GetMapped(ISpecification specification, - Expression> map, Action> configuration = default) - { - return this._readRepository.GetMapped(specification, map, configuration); - } - - public IEnumerable GetFiltered(Expression> filter, - Action> configuration = default) - { - return this._readRepository.GetFiltered(filter, configuration); - } - - public TEntity GetFirst(Expression> filter, Action> configuration = default) - { - return this._readRepository.GetFirst(filter, configuration); - } - - public TEntity GetFirst(ISpecification specification, Action> configuration = default) - { - return this._readRepository.GetFirst(specification, configuration); - } - - public TResult GetFirstMapped(Expression> filter, Expression> map, Action> configuration = default) - { - return this._readRepository.GetFirstMapped(filter, map, configuration); - } - - public TResult GetFirstMapped(ISpecification specification, Expression> map, Action> configuration = default) - { - return this._readRepository.GetFirstMapped(specification, map, configuration); - } - - public IEnumerable GetPaged(int limit, Action> configuration = default) - { - return this._readRepository.GetPaged(limit, configuration); - } - - public IEnumerable GetPaged(ISpecification specification, int limit, - Action> configuration = default) - { - return this._readRepository.GetPaged(specification, limit, configuration); - } - - public IEnumerable GetPaged(Expression> filter, int limit, - Action> configuration = default) - { - return this._readRepository.GetPaged(filter, limit, configuration); - } - - public IEnumerable GetPaged(int pageIndex, int pageSize, Action> configuration = default) - { - return this._readRepository.GetPaged(pageIndex, pageSize, configuration); - } - - public IEnumerable GetPaged(ISpecification specification, int pageIndex, int pageSize, - Action> configuration = default) - { - return this._readRepository.GetPaged(specification, pageIndex, pageSize, configuration); - } - - public IEnumerable GetPaged(Expression> filter, int pageIndex, int pageSize, - Action> configuration = default) - { - return this._readRepository.GetPaged(filter, pageIndex, pageSize, configuration); - } - - public TEntity GetSingle(Expression> filter, Action> configuration = default) - { - return this._readRepository.GetSingle(filter, configuration); - } - - public TEntity GetSingle(ISpecification specification, Action> configuration = default) - { - return this._readRepository.GetSingle(specification, configuration); + return _writeRepository.DeleteMany(specification); } public void Merge(TEntity persisted, TEntity current) { - this._writeRepository.Merge(persisted, current); + _writeRepository.Merge(persisted, current); } public void Modify(TEntity item) { - this._writeRepository.Modify(item); + _writeRepository.Modify(item); } public void Remove(TEntity item) { - this._writeRepository.Remove(item); + _writeRepository.Remove(item); } public void TrackItem(TEntity item) { - this._writeRepository.TrackItem(item); + _writeRepository.TrackItem(item); } public long UpdateMany(Expression> filter, Expression> updateFactory) { - return this._writeRepository.UpdateMany(filter, updateFactory); + return _writeRepository.UpdateMany(filter, updateFactory); } public long UpdateMany(ISpecification specification, Expression> updateFactory) { - return this._writeRepository.UpdateMany(specification, updateFactory); + return _writeRepository.UpdateMany(specification, updateFactory); } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { - if (Disposed) - { - return; - } - if (disposing) { - this._readRepository?.Dispose(); - this._writeRepository?.Dispose(); - // The UnitOfWork is injected, not created here, so its creator owns its lifetime — the DI - // container (which registers the UoW and the repository together) or the caller that built - // it. Disposing it here disposed the shared DbContext out from under the other repositories - // in the same scope and double-disposed it alongside the container. + _writeRepository?.Dispose(); } - Disposed = true; + base.Dispose(disposing); } } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs index 6a780f2..ac965d3 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -7,7 +7,7 @@ using System.Threading.Tasks; using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.Repository.Options; using eQuantic.Core.Data.Repository.Read; using eQuantic.Linq.Specification; using Microsoft.EntityFrameworkCore; @@ -15,807 +15,223 @@ namespace eQuantic.Core.Data.EntityFramework.Repository.Read; [ExcludeFromCodeCoverage] -public class AsyncQueryableReadRepository : - QueryableReadRepository, - IAsyncQueryableReadRepository, - IAsyncReadRepository, TEntity, TKey> - where TUnitOfWork : class, IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class AsyncQueryableReadRepository : + QueryableReadRepository, + IAsyncQueryableReadRepository + where TEntity : class, IEntity { + private const string FilterExpressionCannotBeNull = "Filter expression cannot be null"; + private const string SpecificationCannotBeNull = "Specification cannot be null"; + private const string MapCannotBeNull = "Map expression cannot be null"; - public AsyncQueryableReadRepository(TUnitOfWork unitOfWork) : base(unitOfWork) + public AsyncQueryableReadRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { } - public Task> AllMatchingAsync( - ISpecification specification, - Action> configuration = default) - { - return AllMatchingAsync(specification, configuration, CancellationToken.None); - } - - public async Task> AllMatchingAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return await GetQueryable(configuration, query => - query - .Where(specification.SatisfiedBy())) - .ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public Task> AllMatchingAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return AllMatchingAsync(specification, (Action>)null, cancellationToken); - } - - public Task CountAsync(CancellationToken cancellationToken = default) - { - return GetSet().LongCountAsync(cancellationToken); - } - - public Task CountAsync( - ISpecification specification, + public async Task GetAsync(TKey id, QueryOptions options = null, CancellationToken cancellationToken = default) { - if (specification == null) + if (id is null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(id)); } - return this.CountAsync(specification.SatisfiedBy(), cancellationToken); - } - - public Task CountAsync( - Expression> filter, - CancellationToken cancellationToken = default) - { - return GetSet().LongCountAsync(filter, cancellationToken); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } + if (options == null) + { + return await GetSet().FindAsync(id, cancellationToken).ConfigureAwait(false); + } - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); + var idExpression = GetSet().GetExpression(id); + return await GetSet().GetQueryable(options, query => query.Where(idExpression)) + .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); } - public Task AllAsync( - ISpecification specification, - Action> configuration = default) + public async Task> GetAllAsync(QueryOptions options = null, + CancellationToken cancellationToken = default) { - return AllAsync(specification, configuration, CancellationToken.None); + return await GetSet().GetQueryable(options).ToListAsync(cancellationToken).ConfigureAwait(false); } - public Task AllAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) + public async Task> GetFilteredAsync(Expression> filter, + QueryOptions options = null, CancellationToken cancellationToken = default) { - if (specification == null) + if (filter == null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); } - return AllAsync(specification.SatisfiedBy(), configuration, cancellationToken); - } - - public Task AllAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return AllAsync(specification, (Action>)null, cancellationToken); - } - - public Task AllAsync( - Expression> filter, - Action> configuration = default) - { - return AllAsync(filter, configuration, CancellationToken.None); - } - - public Task AllAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return GetQueryable(configuration, e => e).AllAsync(filter, cancellationToken); - } - - public Task AllAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return AllAsync(filter, (Action>)null, cancellationToken); - } - - public Task AnyAsync( - Action> configuration = default) - { - return AnyAsync(configuration, CancellationToken.None); - } - - public Task AnyAsync( - Action> configuration, - CancellationToken cancellationToken) - { - return GetQueryable(configuration, e => e).AnyAsync(cancellationToken); - } - - public Task AnyAsync( - CancellationToken cancellationToken) - { - return AnyAsync((Action>)null, cancellationToken); + return await GetSet().GetQueryable(options, query => query.Where(filter)) + .ToListAsync(cancellationToken).ConfigureAwait(false); } - public Task AnyAsync( - ISpecification specification, - Action> configuration = default) - { - return AnyAsync(specification, configuration, CancellationToken.None); - } - - public Task AnyAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) + public async Task> AllMatchingAsync(ISpecification specification, + QueryOptions options = null, CancellationToken cancellationToken = default) { if (specification == null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); } - return AnyAsync(specification.SatisfiedBy(), configuration, cancellationToken); - } - - public Task AnyAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return AnyAsync(specification, (Action>)null, cancellationToken); + return await GetSet().GetQueryable(options, query => query.Where(specification.SatisfiedBy())) + .ToListAsync(cancellationToken).ConfigureAwait(false); } - public Task AnyAsync( - Expression> filter, - Action> configuration = default) + public async Task> GetMappedAsync(Expression> map, + QueryOptions options = null, CancellationToken cancellationToken = default) { - return AnyAsync(filter, configuration, CancellationToken.None); - } - - public Task AnyAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return GetQueryable(configuration, query => query.Where(filter)).AnyAsync(cancellationToken); - } - - public Task AnyAsync( - Expression> filter, - CancellationToken cancellationToken = default) - { - return AnyAsync(filter, (Action>)null, cancellationToken); - } + if (map == null) + { + throw new ArgumentNullException(nameof(map), MapCannotBeNull); + } - public Task> GetAllAsync( - Action> configuration = default) - { - return GetAllAsync(configuration, CancellationToken.None); - } - - public async Task> GetAllAsync( - Action> configuration, - CancellationToken cancellationToken) - { - // NOTE: the Where(_ => true) is load-bearing, not redundant. GetQueryable can return the - // SetBase wrapper (which does not implement IAsyncEnumerable); composing a Where turns it into - // a real EF IQueryable so ToListAsync works. Removing it breaks the async path. - return await GetQueryable(configuration, query => query.Where(_ => true)) + return await GetSet().GetQueryable(options).Select(map) .ToListAsync(cancellationToken).ConfigureAwait(false); } - - public Task> GetAllAsync( - CancellationToken cancellationToken = default) - { - return GetAllAsync((Action>)null, cancellationToken); - } - public Task GetAsync( - TKey id, - Action> configuration = default) + public Task GetFirstAsync(QueryOptions options, CancellationToken cancellationToken = default) { - return GetAsync(id, configuration, CancellationToken.None); + return GetSet().GetQueryable(options).FirstOrDefaultAsync(cancellationToken); } - public Task GetAsync( - TKey id, - Action> configuration, - CancellationToken cancellationToken) + public Task GetFirstMappedAsync(Expression> map, + QueryOptions options, CancellationToken cancellationToken = default) { - if (id is null) + if (map == null) { - throw new ArgumentNullException(nameof(id)); + throw new ArgumentNullException(nameof(map), MapCannotBeNull); } - return GetInternalAsync(id, configuration, cancellationToken); - } - - public Task GetAsync( - TKey id, - CancellationToken cancellationToken) - { - return GetAsync(id, (Action>)null, cancellationToken); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - Action> configuration = default) - { - return GetMappedAsync(filter, map, configuration, CancellationToken.None); - } - - public async Task> GetMappedAsync( - Expression> filter, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return await GetQueryable(configuration, query => query.Where(filter)) - .Select(map) - .ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - CancellationToken cancellationToken) - { - return GetMappedAsync(filter, map, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).Select(map).FirstOrDefaultAsync(cancellationToken); } - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration = default) + public Task GetSingleAsync(QueryOptions options, CancellationToken cancellationToken = default) { - return GetMappedAsync(specification, map, configuration, CancellationToken.None); + return GetSet().GetQueryable(options).SingleOrDefaultAsync(cancellationToken); } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) + + public async Task> GetPagedAsync(PageRequest page, QueryOptions options = null, + CancellationToken cancellationToken = default) { - if (specification == null) + if (page == null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(page)); } - return GetMappedAsync(specification.SatisfiedBy(), map, configuration, cancellationToken); - } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - CancellationToken cancellationToken = default) - { - return GetMappedAsync(specification, map, (Action>)null, cancellationToken); - } - - public Task> GetFilteredAsync( - Expression> filter, - Action> configuration = default) - { - return GetFilteredAsync(filter, configuration, CancellationToken.None); - } - - public async Task> GetFilteredAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return await GetQueryable(configuration, query => query.Where(filter)) + var query = GetSet().GetQueryable(options); + var totalCount = await query.LongCountAsync(cancellationToken).ConfigureAwait(false); + var items = await query + .OrderByPrimaryKeyIfUnordered(GetSet().DbContext) + .Skip(page.Skip) + .Take(page.Take) .ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public Task> GetFilteredAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return GetFilteredAsync(filter, (Action>)null, cancellationToken); - } - - public Task GetFirstAsync( - Expression> filter, - Action> configuration = default) - { - return GetFirstAsync(filter, configuration, CancellationToken.None); - } - - public async Task GetFirstAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return await GetQueryable(configuration, query => query.Where(filter)) - .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); - } - - public Task GetFirstAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return GetFirstAsync(filter, (Action>)null, cancellationToken); - } - public Task GetFirstAsync( - ISpecification specification, - Action> configuration = default) - { - return GetFirstAsync(specification, configuration, CancellationToken.None); + return new PagedResult(items, totalCount, page.PageIndex, page.PageSize); } - public Task GetFirstAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) + public async Task> GetPagedAsync(PageRequest page, + Expression> map, QueryOptions options = null, + CancellationToken cancellationToken = default) { - if (specification == null) + if (page == null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(page)); } - return GetFirstAsync(specification.SatisfiedBy(), configuration, cancellationToken); - } - - public Task GetFirstAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return GetFirstAsync(specification, (Action>)null, cancellationToken); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - Action> configuration = default) - { - return GetFirstMappedAsync(filter, map, configuration, CancellationToken.None); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - if (filter == null) + if (map == null) { - throw new ArgumentNullException(nameof(filter), "Filter expression cannot be null"); + throw new ArgumentNullException(nameof(map), MapCannotBeNull); } - - return GetQueryable(configuration, query => query.Where(filter)) + + var query = GetSet().GetQueryable(options); + var totalCount = await query.LongCountAsync(cancellationToken).ConfigureAwait(false); + var items = await query + .OrderByPrimaryKeyIfUnordered(GetSet().DbContext) + .Skip(page.Skip) + .Take(page.Take) .Select(map) - .FirstOrDefaultAsync(cancellationToken); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - CancellationToken cancellationToken) - { - return GetFirstMappedAsync(filter, map, (Action>)null, cancellationToken); - } + .ToListAsync(cancellationToken).ConfigureAwait(false); - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration = default) - { - return GetFirstMappedAsync(specification, map, configuration, CancellationToken.None); + return new PagedResult(items, totalCount, page.PageIndex, page.PageSize); } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification)); - } - return GetFirstMappedAsync(specification.SatisfiedBy(), map, configuration, cancellationToken); - } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - CancellationToken cancellationToken = default) + public Task CountAsync(QueryOptions options = null, CancellationToken cancellationToken = default) { - return GetFirstMappedAsync(specification, map, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).LongCountAsync(cancellationToken); } - public Task> GetPagedAsync( - int limit, - Action> configuration = default) - { - return GetPagedAsync(limit, configuration, CancellationToken.None); - } - - public Task> GetPagedAsync( - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return GetPagedAsync((Expression>)null, 1, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - int limit, - CancellationToken cancellationToken = default) + public Task AnyAsync(QueryOptions options = null, CancellationToken cancellationToken = default) { - return GetPagedAsync(limit, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).AnyAsync(cancellationToken); } - public Task> GetPagedAsync( - ISpecification specification, - int limit, - Action> configuration = default) - { - return GetPagedAsync(specification, limit, configuration, CancellationToken.None); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - Action> configuration, - CancellationToken cancellationToken) + public Task AllAsync(Expression> predicate, QueryOptions options = null, + CancellationToken cancellationToken = default) { - if (specification == null) + if (predicate == null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(predicate), FilterExpressionCannotBeNull); } - return GetPagedAsync(specification.SatisfiedBy(), 1, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - CancellationToken cancellationToken) - { - return GetPagedAsync(specification, limit, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).AllAsync(predicate, cancellationToken); } - public Task> GetPagedAsync( - Expression> filter, - int limit, - Action> configuration = default) - { - return GetPagedAsync(filter, limit, configuration, CancellationToken.None); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return GetPagedAsync(filter, 1, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetPagedAsync(filter, limit, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - Action> configuration = default) - { - return GetPagedAsync(pageIndex, pageSize, configuration, - CancellationToken.None); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - return GetPagedAsync((Expression>)null, pageIndex, pageSize, configuration, - cancellationToken); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetPagedAsync(pageIndex, pageSize, (Action>)null, - cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - Action> configuration = default) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetPagedAsync(specification, pageIndex, pageSize, configuration, CancellationToken.None); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification)); - } - - return GetPagedAsync(specification.SatisfiedBy(), pageIndex, pageSize, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return GetPagedAsync(specification, pageIndex, pageSize, (Action>)null, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - Action> configuration = default) - { - return GetPagedAsync(filter, pageIndex, pageSize, configuration, CancellationToken.None); - } - - public async Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - var query = GetQueryable(configuration, internalQuery => - { - if (filter != null) - { - internalQuery = internalQuery.Where(filter); - } - - return internalQuery; - }); - - - if (pageIndex < 1) pageIndex = 1; - if (pageSize > 0) - { - query = query.OrderByPrimaryKeyIfUnordered(GetSet().DbContext); - return await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false); - } - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return GetPagedAsync(filter, pageIndex, pageSize, (Action>)null, cancellationToken); - } - - public Task GetSingleAsync( - Expression> filter, - Action> configuration = default) - { - return GetSingleAsync(filter, configuration, CancellationToken.None); - } - - public async Task GetSingleAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return await GetQueryable(configuration, query => query.Where(filter)) - .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - - public Task GetSingleAsync( - Expression> filter, - CancellationToken cancellationToken) + + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetSingleAsync(filter, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task GetSingleAsync( - ISpecification specification, - Action> configuration = default) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetSingleAsync(specification, configuration, CancellationToken.None); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - - public Task GetSingleAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification)); - } - return GetSingleAsync(specification.SatisfiedBy(), configuration, cancellationToken); - } - - public Task GetSingleAsync( - ISpecification specification, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetSingleAsync(specification, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - private async Task GetInternalAsync(TKey id, Action> configuration = default, + public Task SumAsync(Expression> selector, QueryOptions options = null, CancellationToken cancellationToken = default) { - if (configuration == null) - { - return await GetSet().FindAsync(id, cancellationToken).ConfigureAwait(false); - } - - var idExpression = GetSet().GetExpression(id); - return await GetQueryable(configuration, query => query.Where(idExpression)) - .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - - private IQueryable GetQueryable(Action> configuration, - Func, IQueryable> internalQueryAction) + + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetSet().GetQueryable(configuration, internalQueryAction); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs index 59591ba..884586b 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs @@ -1,436 +1,229 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.Repository.Options; using eQuantic.Core.Data.Repository.Read; using eQuantic.Linq.Specification; namespace eQuantic.Core.Data.EntityFramework.Repository.Read; [ExcludeFromCodeCoverage] -public class QueryableReadRepository : - IQueryableReadRepository, - IReadRepository, TEntity, TKey> - where TUnitOfWork : IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class QueryableReadRepository : + IQueryableReadRepository + where TEntity : class, IEntity { internal SetBase _dbSet; private bool _disposed; - /// - /// Whether this repository owns the injected 's lifetime. Defaults to - /// false: the UnitOfWork is provided by its creator (the DI container or the caller), and - /// disposing the repository must not dispose a UnitOfWork it did not create. - /// - internal bool OwnUnitOfWork { get; set; } = false; - private const string SpecificationCannotBeNull = "Specification cannot be null"; private const string FilterExpressionCannotBeNull = "Filter expression cannot be null"; + private const string SpecificationCannotBeNull = "Specification cannot be null"; + /// /// Creates a new instance of the read repository /// /// Associated Unit Of Work - public QueryableReadRepository(TUnitOfWork unitOfWork) + public QueryableReadRepository(IQueryableUnitOfWork unitOfWork) { UnitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); } /// - /// + /// The associated queryable unit of work. /// - public TUnitOfWork UnitOfWork { get; private set; } + public IQueryableUnitOfWork UnitOfWork { get; private set; } - public IEnumerable AllMatching(ISpecification specification, - Action> configuration = default) + public TEntity Get(TKey id, QueryOptions options = null) { - if (specification == null) + if (id is null) { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); + throw new ArgumentNullException(nameof(id)); } - return GetQueryable(configuration, query => query.Where(specification.SatisfiedBy())); - } - - public long Count() - { - return GetSet().LongCount(); - } - - public long Count(ISpecification specification) - { - if (specification == null) + if (options == null) { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); + return GetSet().Find(id); } - return this.Count(specification.SatisfiedBy()); - } - - public long Count(Expression> filter) - { - return filter == null ? throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull) : GetSet().LongCount(filter); - } - - - public int Sum(Expression> source) - { - return GetSet().Sum(source); - } - public int Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public int Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public int? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public int? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public int? Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public long Sum(Expression> source) - { - return GetSet().Sum(source); - } - public long Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public long Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public long? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public long? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public long? Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public double Sum(Expression> source) - { - return GetSet().Sum(source); - } - public double Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public double Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public double? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public double? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public double? Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); + var idExpression = GetSet().GetExpression(id); + return GetSet().GetQueryable(options, query => query.Where(idExpression)).SingleOrDefault(); } - public float Sum(Expression> source) - { - return GetSet().Sum(source); - } - public float Sum(ISpecification specification, Expression> source) + public IEnumerable GetAll(QueryOptions options = null) { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public float Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); + return GetSet().GetQueryable(options); } - public float? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public float? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public float? Sum(Expression> filter, Expression> source) + public IEnumerable GetFiltered(Expression> filter, QueryOptions options = null) { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } + if (filter == null) + { + throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); + } - public decimal Sum(Expression> source) - { - return GetSet().Sum(source); - } - public decimal Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public decimal Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); + return GetSet().GetQueryable(options, query => query.Where(filter)); } - public decimal? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public decimal? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public decimal? Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public bool All(ISpecification specification, Action> configuration = default) + public IEnumerable AllMatching(ISpecification specification, QueryOptions options = null) { if (specification == null) { throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); } - return this.All(specification.SatisfiedBy(), configuration); + return GetSet().GetQueryable(options, query => query.Where(specification.SatisfiedBy())); } - public bool All(Expression> filter, Action> configuration = default) + public IEnumerable GetMapped(Expression> map, QueryOptions options = null) { - if (filter == null) + if (map == null) { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); + throw new ArgumentNullException(nameof(map)); } - return GetQueryable(configuration, _ => _).All(filter); + return GetSet().GetQueryable(options).Select(map); } - public bool Any(Action> configuration = default) + public TEntity GetFirst(QueryOptions options) { - return GetQueryable(configuration, _ => _).Any(); + return GetSet().GetQueryable(options).FirstOrDefault(); } - public bool Any(ISpecification specification, Action> configuration = default) + public TResult GetFirstMapped(Expression> map, QueryOptions options) { - if (specification == null) + if (map == null) { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); + throw new ArgumentNullException(nameof(map)); } - return this.Any(specification.SatisfiedBy(), configuration); + return GetSet().GetQueryable(options).Select(map).FirstOrDefault(); } - public bool Any(Expression> filter, Action> configuration = default) + public TEntity GetSingle(QueryOptions options) { - if (filter == null) + return GetSet().GetQueryable(options).SingleOrDefault(); + } + + public PagedResult GetPaged(PageRequest page, QueryOptions options = null) + { + if (page == null) { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); + throw new ArgumentNullException(nameof(page)); } - return GetQueryable(configuration, query => query.Where(filter)).Any(); - } + var query = GetSet().GetQueryable(options); + var totalCount = query.LongCount(); + var items = query + .OrderByPrimaryKeyIfUnordered(GetSet().DbContext) + .Skip(page.Skip) + .Take(page.Take) + .ToList(); - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); + return new PagedResult(items, totalCount, page.PageIndex, page.PageSize); } - public TEntity Get(TKey id, Action> configuration = default) + public PagedResult GetPaged(PageRequest page, Expression> map, + QueryOptions options = null) { - if (id is null) + if (page == null) { - throw new ArgumentNullException(nameof(id)); + throw new ArgumentNullException(nameof(page)); } - if (configuration == null) + if (map == null) { - return GetSet().Find(id); + throw new ArgumentNullException(nameof(map)); } - var idExpression = GetSet().GetExpression(id); - return GetQueryable(configuration, query => query.Where(idExpression)) - .SingleOrDefault(); - } - - public IEnumerable GetAll(Action> configuration = default) - { - return GetQueryable(configuration, query => query); - } + var query = GetSet().GetQueryable(options); + var totalCount = query.LongCount(); + var items = query + .OrderByPrimaryKeyIfUnordered(GetSet().DbContext) + .Skip(page.Skip) + .Take(page.Take) + .Select(map) + .ToList(); - public IEnumerable GetMapped(Expression> filter, - Expression> map, Action> configuration = default) - { - return GetQueryable(configuration, query => query.Where(filter)).Select(map); + return new PagedResult(items, totalCount, page.PageIndex, page.PageSize); } - public IEnumerable GetMapped(ISpecification specification, - Expression> map, Action> configuration = default) + public long Count(QueryOptions options = null) { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return this.GetMapped(specification.SatisfiedBy(), map, configuration); + return GetSet().GetQueryable(options).LongCount(); } - public IEnumerable GetFiltered(Expression> filter, - Action> configuration = default) + public bool Any(QueryOptions options = null) { - if (filter == null) - { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); - } - - return GetQueryable(configuration, query => query.Where(filter)); + return GetSet().GetQueryable(options).Any(); } - public TEntity GetFirst(Expression> filter, Action> configuration = default) + public bool All(Expression> predicate, QueryOptions options = null) { - if (filter == null) + if (predicate == null) { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); + throw new ArgumentNullException(nameof(predicate), FilterExpressionCannotBeNull); } - return GetQueryable(configuration, query => query.Where(filter)).FirstOrDefault(); + return GetSet().GetQueryable(options).All(predicate); } - public TEntity GetFirst(ISpecification specification, Action> configuration = default) + public int Sum(Expression> selector, QueryOptions options = null) { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification),SpecificationCannotBeNull); - } - - return GetQueryable(configuration, query => query.Where(specification.SatisfiedBy())).FirstOrDefault(); + return GetSet().GetQueryable(options).Sum(selector); } - public TResult GetFirstMapped(Expression> filter, - Expression> map, Action> configuration = default) + public int? Sum(Expression> selector, QueryOptions options = null) { - if (filter == null) - { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); - } - return GetQueryable(configuration, query => query.Where(filter)) - .Select(map) - .FirstOrDefault(); + return GetSet().GetQueryable(options).Sum(selector); } - public TResult GetFirstMapped(ISpecification specification, - Expression> map, Action> configuration = default) + public long Sum(Expression> selector, QueryOptions options = null) { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return this.GetFirstMapped(specification.SatisfiedBy(), map, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(int limit, Action> configuration = default) + public long? Sum(Expression> selector, QueryOptions options = null) { - return GetPaged((Expression>)null, 1, limit, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(ISpecification specification, int limit, - Action> configuration = default) + public double Sum(Expression> selector, QueryOptions options = null) { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return GetPaged(specification.SatisfiedBy(), 1, limit, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(Expression> filter, int limit, - Action> configuration = default) + public double? Sum(Expression> selector, QueryOptions options = null) { - return GetPaged(filter, 1, limit, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(int pageIndex, int pageSize, Action> configuration = default) + public float Sum(Expression> selector, QueryOptions options = null) { - return GetPaged((Expression>)null, pageIndex, pageSize, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(ISpecification specification, int pageIndex, int pageSize, - Action> configuration = default) + public float? Sum(Expression> selector, QueryOptions options = null) { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return GetPaged(specification.SatisfiedBy(), pageIndex, pageSize, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(Expression> filter, int pageIndex, int pageSize, - Action> configuration = default) + public decimal Sum(Expression> selector, QueryOptions options = null) { - var query = GetQueryable(configuration, internalQuery => - { - if (filter != null) - { - internalQuery = internalQuery.Where(filter); - } - - return internalQuery; - }); - if (pageIndex < 1) pageIndex = 1; - if (pageSize <= 0) - { - return query; - } - - query = query.OrderByPrimaryKeyIfUnordered(GetSet().DbContext); - return query.Skip((pageIndex - 1) * pageSize).Take(pageSize); + return GetSet().GetQueryable(options).Sum(selector); } - public TEntity GetSingle(Expression> filter, Action> configuration = default) + public decimal? Sum(Expression> selector, QueryOptions options = null) { - if (filter == null) - { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); - } - - return GetQueryable(configuration, query => query.Where(filter)) - .SingleOrDefault(); + return GetSet().GetQueryable(options).Sum(selector); } - public TEntity GetSingle(ISpecification specification, Action> configuration = default) + public void Dispose() { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return GetQueryable(configuration, query => - query - .Where(specification.SatisfiedBy())) - .SingleOrDefault(); + Dispose(true); + GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) @@ -440,20 +233,12 @@ protected virtual void Dispose(bool disposing) return; } - if (disposing && OwnUnitOfWork) - { - UnitOfWork?.Dispose(); - } - + // The UnitOfWork is injected, not created here, so its creator (the DI container or the caller) + // owns its lifetime. Disposing it here would tear down the shared DbContext out from under the + // other repositories in the same scope. _disposed = true; } - private IQueryable GetQueryable(Action> configuration, - Func, IQueryable> internalQueryAction) - { - return GetSet().GetQueryable(configuration, internalQueryAction); - } - internal virtual SetBase GetSet() { return _dbSet ??= (SetBase)UnitOfWork.CreateSet(); diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs index f75f0da..17696ca 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs @@ -7,13 +7,13 @@ using System.Threading.Tasks; using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.Repository.Options; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace eQuantic.Core.Data.EntityFramework.Repository; -public abstract class SetBase : Data.Repository.ISet where TEntity : class, IEntity, new() +public abstract class SetBase : Data.Repository.ISet where TEntity : class, IEntity { internal readonly DbContext DbContext; @@ -38,7 +38,7 @@ public virtual void AddRange(params TEntity[] entities) { InternalDbSet.AddRange(entities); } - + public virtual Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) { return InternalDbSet.AddRangeAsync(entities, cancellationToken); @@ -48,7 +48,7 @@ public virtual Task AddRangeAsync(params TEntity[] entities) { return InternalDbSet.AddRangeAsync(entities); } - + public virtual void ApplyCurrentValues(TEntity original, TEntity current) { //if it is not attached, attach original and set current values @@ -84,7 +84,7 @@ public override bool Equals(object obj) { return InternalDbSet.Equals(obj); } - + public virtual IEnumerable Execute() { return InternalDbSet.ToList(); @@ -158,7 +158,7 @@ public virtual void SetModified(TEntity item) { return; } - + //this operation also attach item in object state manager entry.State = EntityState.Modified; } @@ -187,21 +187,28 @@ public virtual void UpdateRange(params TEntity[] entities) { InternalDbSet.UpdateRange(entities); } - + internal Expression> GetExpression(TKey id) { return DbContext.GetFindByKeyExpression(id); } - - protected static TConfig GetConfig(Action configuration) - where TConfig : Configuration, new() - { - var config = new TConfig(); - configuration?.Invoke(config); - return config; - } - public abstract IQueryable GetQueryable(Action configuration, - Func, IQueryable> internalQueryAction) - where TConfig : Configuration, new(); -} \ No newline at end of file + /// + /// Shapes a query from this set using the supplied . The + /// translation (before-customization → specification/filter → the per-call + /// → includes → sortings → no-tracking → ignore + /// query-filters → tag → after-customization) lives in + /// so relational and document providers + /// share it. The query starts from the underlying (not the set + /// wrapper) so it is a real EF queryable and the async materialization operators work even when no + /// shaping is applied. + /// + /// The query options, or null for no shaping. + /// An optional per-call transformation (e.g. an explicit filter). + /// The shaped query. + internal IQueryable GetQueryable(QueryOptions options, + Func, IQueryable> internalQueryAction = null) + { + return InternalDbSet.ApplyOptions(options, internalQueryAction); + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/AsyncWriteRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/AsyncWriteRepository.cs index be90da4..2c62fd5 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/AsyncWriteRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/AsyncWriteRepository.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -8,23 +9,32 @@ namespace eQuantic.Core.Data.EntityFramework.Repository.Write; -public class AsyncWriteRepository : WriteRepository, - IAsyncWriteRepository - where TUnitOfWork : IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class AsyncWriteRepository : WriteRepository, + IAsyncWriteRepository + where TEntity : class, IEntity { - public AsyncWriteRepository(TUnitOfWork unitOfWork) : base(unitOfWork) + public AsyncWriteRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { } - public Task AddAsync(TEntity item) + public Task AddAsync(TEntity item, CancellationToken cancellationToken = default) { if (item == null) { throw new ArgumentNullException(nameof(item)); } - return GetSet().InsertAsync(item); + return GetSet().InsertAsync(item, cancellationToken); + } + + public Task AddRangeAsync(IEnumerable items, CancellationToken cancellationToken = default) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + return GetSet().AddRangeAsync(items, cancellationToken); } public Task DeleteManyAsync(Expression> filter, @@ -93,5 +103,4 @@ public Task UpdateManyAsync(ISpecification specification, return this.UpdateManyAsync(specification.SatisfiedBy(), updateFactory, cancellationToken); } - } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs index 876b712..71f22cb 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq.Expressions; using eQuantic.Core.Data.Repository; using eQuantic.Core.Data.Repository.Write; @@ -6,26 +7,21 @@ namespace eQuantic.Core.Data.EntityFramework.Repository.Write; -public class WriteRepository : IWriteRepository - where TUnitOfWork : IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class WriteRepository : IWriteRepository + where TEntity : class, IEntity { internal SetBase _dbSet; private bool _disposed; - /// - /// Whether this repository owns the injected 's lifetime. Defaults to - /// false: the UnitOfWork is provided by its creator (the DI container or the caller), and - /// disposing the repository must not dispose a UnitOfWork it did not create. - /// - internal bool OwnUnitOfWork { get; set; } = false; - - public WriteRepository(TUnitOfWork unitOfWork) + public WriteRepository(IQueryableUnitOfWork unitOfWork) { UnitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); } - public TUnitOfWork UnitOfWork { get; private set; } + /// + /// The associated queryable unit of work. + /// + public IQueryableUnitOfWork UnitOfWork { get; private set; } public void Add(TEntity item) { @@ -37,6 +33,16 @@ public void Add(TEntity item) GetSet().Insert(item); } + public void AddRange(IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + GetSet().AddRange(items); + } + public long DeleteMany(Expression> filter) { if (filter == null) @@ -57,12 +63,6 @@ public long DeleteMany(ISpecification specification) return DeleteMany(specification.SatisfiedBy()); } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - public void Merge(TEntity persisted, TEntity current) { GetSet().ApplyCurrentValues(persisted, current); @@ -127,6 +127,12 @@ public long UpdateMany(ISpecification specification, Expression Date: Mon, 20 Jul 2026 15:42:56 +0100 Subject: [PATCH 23/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(relational)?= =?UTF-8?q?:=20rehome=20SQL=20abstractions=20and=20adapt=20to=20v5=20contr?= =?UTF-8?q?acts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/ServiceCollectionExtensions.cs | 51 +++++++ .../Extensions/SqlConfigurationExtensions.cs | 2 +- .../Repository/RelationalSet.cs | 63 +-------- .../Repository/RelationalSqlExecutor.cs | 3 +- .../Repository/RelationalUnitOfWork.cs | 50 +++---- .../Sql/IAsyncSqlExecutor.cs | 91 ++++++++++++ .../Sql/ISqlExecutor.cs | 125 +++++++++++++++++ .../Sql/ISqlUnitOfWork.cs | 132 ++++++++++++++++++ .../Sql/ParamValue.cs | 43 ++++++ .../Sql/SqlConfiguration.cs | 61 ++++++++ 10 files changed, 528 insertions(+), 93 deletions(-) create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/Sql/IAsyncSqlExecutor.cs create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlExecutor.cs create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlUnitOfWork.cs create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ParamValue.cs create mode 100644 src/eQuantic.Core.Data.EntityFramework.Relational/Sql/SqlConfiguration.cs diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/ServiceCollectionExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..b5d5e97 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,51 @@ +using System; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; +using eQuantic.Core.Data.EntityFramework.Repository.Options; +using eQuantic.Core.Data.Repository; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using BaseServiceCollectionExtensions = eQuantic.Core.Data.EntityFramework.Repository.Extensions.ServiceCollectionExtensions; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; + +/// +/// Relational registration helpers. These wrap the SQL-agnostic base registration and additionally +/// expose when the unit of work implements it. The base +/// AddQueryableRepositories no longer knows about because that +/// contract moved into the Relational provider in v5. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Registers the relational unit of work, the generic repositories and, when + /// implements it, . + /// + /// The unit of work interface. + /// The unit of work implementation. + /// The service collection. + /// Optional repository options (lifetime, assembly scanning). + /// The service collection for chaining. + public static IServiceCollection AddRelationalRepositories( + this IServiceCollection services, Action options = null) + where TUnitOfWorkInterface : IQueryableUnitOfWork + where TUnitOfWorkImpl : class, TUnitOfWorkInterface + { + options ??= _ => { }; + + BaseServiceCollectionExtensions.AddQueryableRepositories(services, options); + + var repoOptions = new RepositoryOptions(); + options(repoOptions); + var lifetime = repoOptions.GetLifetime(); + + // Only expose ISqlUnitOfWork when the implementation actually provides it. Registering it + // unconditionally would make resolving ISqlUnitOfWork throw for a non-SQL unit of work. + if (typeof(ISqlUnitOfWork).IsAssignableFrom(typeof(TUnitOfWorkImpl))) + { + services.TryAdd(new ServiceDescriptor(typeof(ISqlUnitOfWork), + sp => sp.GetRequiredService(), lifetime)); + } + + return services; + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs index b1583bd..7ce7350 100644 --- a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs @@ -1,4 +1,4 @@ -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; using Microsoft.EntityFrameworkCore; namespace eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs index b56faa3..07e719e 100644 --- a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs @@ -6,10 +6,7 @@ using System.Threading; using System.Threading.Tasks; using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; #if NET6_0 || NETSTANDARD2_1 using Z.EntityFramework.Plus; @@ -20,7 +17,7 @@ namespace eQuantic.Core.Data.EntityFramework.Relational.Repository; /// /// The shared relational entity set used by the SqlServer, PostgreSql and MySql providers. /// -public class RelationalSet : SetBase where TEntity : class, IEntity, new() +public class RelationalSet : SetBase where TEntity : class, IEntity { public RelationalSet(DbContext context) : base(context) { @@ -229,62 +226,4 @@ private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) await LoadCascadeAsync(props, nextObj, index + 1).ConfigureAwait(false); } } - - internal Expression> GetExpression(TKey id) - { - return DbContext.GetFindByKeyExpression(id); - } - - public override IQueryable GetQueryable(Action configuration, - Func, IQueryable> internalQueryAction) - { - if (configuration == null) - { - return internalQueryAction.Invoke(this); - } - - var config = GetConfig(configuration); - var queryableConfig = config as QueryableConfiguration; - - var query = string.IsNullOrEmpty(queryableConfig?.SqlRaw) ? this : InternalDbSet.FromSqlRaw(queryableConfig.SqlRaw); - - if (config.HasNoTracking) - { - query = query.AsNoTracking(); - } - - if (config.Properties?.Any() == true) - { - query = query.IncludeMany(config.Properties.ToArray()); - } - - if (queryableConfig?.IgnoreQueryFilters == true) - { - query = query.IgnoreQueryFilters(); - } - - if (!string.IsNullOrEmpty(config.Tag)) - { - query = query.TagWith(config.Tag); - } - - if (queryableConfig != null) - { - query = queryableConfig.BeforeCustomization.Invoke(query); - } - - query = internalQueryAction.Invoke(query); - - if (config.SortingColumns.Any()) - { - query = query.OrderBy(config.SortingColumns.ToArray()); - } - - if (queryableConfig != null) - { - query = queryableConfig.AfterCustomization.Invoke(query); - } - - return query; - } } diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs index 5db22d6..059aebc 100644 --- a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs @@ -8,8 +8,7 @@ using System.Threading; using System.Threading.Tasks; using eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Sql; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs index 1038997..d17f2ec 100644 --- a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs @@ -4,9 +4,9 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; using eQuantic.Core.Data.Repository; using eQuantic.Core.Data.Repository.Options; -using eQuantic.Core.Data.Repository.Sql; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -119,14 +119,15 @@ public Task CommitAsync(Action options, CancellationToken canc return CommitAsync(cancellationToken); } - Data.Repository.ISet IQueryableUnitOfWork.CreateSet() => InternalCreateSet(); + public Data.Repository.ISet CreateSet() where TEntity : class, IEntity => + InternalCreateSet(); - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() + public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity { ((RelationalSet)InternalCreateSet()).ApplyCurrentValues(original, current); } - public void Attach(TEntity item) where TEntity : class, IEntity, new() + public void Attach(TEntity item) where TEntity : class, IEntity { ((RelationalSet)InternalCreateSet()).Attach(item); } @@ -137,27 +138,27 @@ public IEnumerable GetPendingMigrations() } public void LoadProperty(TEntity item, Expression> selector) - where TEntity : class, IEntity, new() + where TEntity : class, IEntity where TComplexProperty : class { ((RelationalSet)InternalCreateSet()).LoadProperty(item, selector); } public void LoadProperty(TEntity item, string propertyName) - where TEntity : class, IEntity, new() + where TEntity : class, IEntity { ((RelationalSet)InternalCreateSet()).LoadProperty(item, propertyName); } public Task LoadPropertyAsync(TEntity item, Expression> selector, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() + where TEntity : class, IEntity where TComplexProperty : class { return ((RelationalSet)InternalCreateSet()).LoadPropertyAsync(item, selector, cancellationToken); } public Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() + where TEntity : class, IEntity { return ((RelationalSet)InternalCreateSet()).LoadPropertyAsync(item, propertyName, cancellationToken); } @@ -229,42 +230,35 @@ public void UpdateDatabase() } } - public virtual Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => - InternalCreateSet(); - public virtual SaveOptions GetSaveOptions() { return new SaveOptions(); } - public virtual IRepository GetRepository() - where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork + public virtual IRepository GetRepository() + where TEntity : class, IEntity { - var repo = _serviceProvider.GetRequiredService>(); - return repo; + return _serviceProvider.GetRequiredService>(); } - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork + public IAsyncRepository GetAsyncRepository() + where TEntity : class, IEntity { - return _serviceProvider.GetRequiredService>(); + return _serviceProvider.GetRequiredService>(); } - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + public IQueryableRepository GetQueryableRepository() + where TEntity : class, IEntity { - return _serviceProvider.GetRequiredService>(); + return _serviceProvider.GetRequiredService>(); } - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + public IAsyncQueryableRepository GetAsyncQueryableRepository() + where TEntity : class, IEntity { - return _serviceProvider.GetRequiredService>(); + return _serviceProvider.GetRequiredService>(); } - private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => + private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity => new RelationalSet(_context); } diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/IAsyncSqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/IAsyncSqlExecutor.cs new file mode 100644 index 0000000..e3213fc --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/IAsyncSqlExecutor.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// The async sql executor interface +/// +/// +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Sql +/// namespace (dropped in eQuantic.Core.Data v5). +/// +public interface IAsyncSqlExecutor where TConfig : SqlConfiguration +{ + /// + /// Begins the transaction asynchronous. + /// + /// The cancellation token. + /// + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Commits the transaction asynchronous. + /// + /// The cancellation token. + /// + Task CommitTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Rollbacks the transaction asynchronous. + /// + /// The cancellation token. + /// + Task RollbackTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the raw SQL asynchronous. + /// + /// + /// The SQL. + /// The map. + /// The configuration. + /// The cancellation token. + /// + Task> ExecuteRawSqlAsync(string sql, Func map, Action config = null, CancellationToken cancellationToken = default); + + /// + /// Execute async arbitrary command into underlying persistence store + /// + /// Command to execute + /// + /// The cancellation token. + /// The number of affected records + Task ExecuteCommandAsync(string sqlCommand, Action config = null, CancellationToken cancellationToken = default); + + /// + /// Execute Function Async. + /// + /// + /// + /// + /// The cancellation token. + /// + Task ExecuteFunctionAsync(string name, Action config = null, CancellationToken cancellationToken = default) + where TResult : class; + + /// + /// Execute Procedure Async. + /// + /// + /// + /// The cancellation token. + /// + Task ExecuteProcedureAsync(string name, Action config = null, + CancellationToken cancellationToken = default); + + /// + /// Use transaction async. + /// + /// + /// The cancellation token. + /// + Task UseTransactionAsync(DbTransaction transaction, CancellationToken cancellationToken = default); +} + +public interface IAsyncSqlExecutor : IAsyncSqlExecutor +{ +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlExecutor.cs new file mode 100644 index 0000000..9327179 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlExecutor.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// Base contract for support 'dialect specific queries'. +/// +/// +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Sql +/// namespace (dropped in eQuantic.Core.Data v5). +/// +public interface ISqlExecutor where TConfig : SqlConfiguration +{ + /// + /// Begins the transaction. + /// + void BeginTransaction(); + + /// + /// Commits the transaction. + /// + void CommitTransaction(); + + /// + /// Executes the raw SQL. + /// + /// + /// The SQL. + /// The map. + /// The configuration. + /// + IEnumerable ExecuteRawSql(string sql, Func map, Action config = null); + + /// + /// Execute arbitrary command into underlying persistence store + /// + /// + /// Command to execute + /// + /// SELECT idCustomer,Name FROM dbo.[Customers] WHERE idCustomer > {0} + /// + /// + /// The configuration a vector of parameters values + /// The number of affected records + int ExecuteCommand(string sqlCommand, Action config = null); + + /// + /// Executes a SQL statement using the specified sql command + /// + /// The sql command + /// + /// The number of rows affected. + public int ExecuteNonQuery(string sqlCommand, Action config = null); + + /// + /// + /// + /// + /// + /// + /// + TResult ExecuteFunction(string name, Action config = null) where TResult : class; + + /// + /// + /// + /// + /// + /// + int ExecuteProcedure(string name, Action config = null); + + /// + /// Execute specific query with underlying persistence store + /// + /// Entity type to map query results + /// + /// Dialect Query + /// + /// SELECT idCustomer,Name FROM dbo.[Customers] WHERE idCustomer > {0} + /// + /// + /// The configuration with a vector of parameters values + /// + /// Enumerable results + /// + IEnumerable ExecuteQuery(string sqlQuery, Action config = null) where TEntity : class; + + /// + /// Executes the transaction. + /// + /// The operation. + void ExecuteTransaction(Action operation); + + /// + /// Executes the transaction async. + /// + /// The operation. + /// The cancellation token. + Task ExecuteTransactionAsync(Func operation, CancellationToken cancellationToken = default); + + /// + /// Get transaction. + /// + /// + DbTransaction GetTransaction(); + + /// + /// Rollbacks the transaction. + /// + void RollbackTransaction(); + + /// + /// Use transaction. + /// + /// + void UseTransaction(DbTransaction transaction); +} + +public interface ISqlExecutor : ISqlExecutor +{ +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlUnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlUnitOfWork.cs new file mode 100644 index 0000000..c9de201 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlUnitOfWork.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.Repository; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// The UnitOfWork contract for the EF relational implementation. +/// +/// This contract extends IQueryableUnitOfWork with relational/SQL specific operations. +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Sql +/// namespace (dropped in eQuantic.Core.Data v5). The new() entity constraint that v4 required +/// has been removed to match the v5 surface. +/// +/// +public interface ISqlUnitOfWork : IQueryableUnitOfWork, ISqlExecutor, IAsyncSqlExecutor +{ + /// + /// Apply current values in + /// + /// The type of entity + /// The original entity + /// The current entity + void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity; + + /// + /// Attach this item into "ObjectStateManager" + /// + /// The type of entity + /// The item + void Attach(TEntity item) where TEntity : class, IEntity; + + /// + /// + /// + /// + IEnumerable GetPendingMigrations(); + + /// + /// + /// + /// + /// + /// + /// + /// + void LoadCollection(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) + where TEntity : class + where TElement : class; + + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task LoadCollectionAsync(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) where TEntity : class where TElement : class; + + /// + /// + /// + /// + /// + /// + /// + void LoadProperty(TEntity item, Expression> selector) + where TEntity : class, IEntity + where TComplexProperty : class; + + /// + /// + /// + /// + /// + /// + void LoadProperty(TEntity item, string propertyName) + where TEntity : class, IEntity; + + /// + /// + /// + /// + /// + /// + /// + /// The cancellation token + /// + Task LoadPropertyAsync(TEntity item, + Expression> selector, CancellationToken cancellationToken = default) + where TEntity : class, IEntity + where TComplexProperty : class; + + /// + /// + /// + /// + /// + /// + /// The cancellation token. + /// + Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) + where TEntity : class, IEntity; + + /// + /// Reload this item ignoring cache + /// + /// + /// + void Reload(TEntity item) where TEntity : class; + + /// + /// Set object as modified + /// + /// The type of entity + /// The entity item to set as modifed + void SetModified(TEntity item) where TEntity : class; + + /// + /// + /// + void UpdateDatabase(); +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ParamValue.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ParamValue.cs new file mode 100644 index 0000000..759e670 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ParamValue.cs @@ -0,0 +1,43 @@ +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// A named (or positional) parameter value carried through the relational SQL executor. Values +/// flow into commands as s and are never interpolated +/// into the SQL text. +/// +/// +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Sql +/// namespace (dropped in eQuantic.Core.Data v5). +/// +public sealed class ParamValue +{ + public string Name { get; } + public object Value { get; } + + private ParamValue(string name, object value) : this(value) + { + Name = name; + } + + private ParamValue(object value) + { + Value = value; + } + + public static ParamValue Create(string name, object value) => new ParamValue(name, value); + public static ParamValue Create(object value) => new ParamValue(value); + + public override bool Equals(object obj) + { + return obj is ParamValue paramValue && ( + !string.IsNullOrEmpty(paramValue.Name) + ? paramValue.Name == Name + : paramValue.Name == Name && paramValue.Value.Equals(Value) + ); + } + + public override int GetHashCode() + { + return (Name, Value).GetHashCode(); + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/SqlConfiguration.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/SqlConfiguration.cs new file mode 100644 index 0000000..4fc2cb6 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/SqlConfiguration.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// Configuration for a raw SQL / stored-procedure / function invocation performed through the +/// relational SQL executor. +/// +/// +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Config +/// namespace (dropped in eQuantic.Core.Data v5). +/// +public class SqlConfiguration +{ + public HashSet Parameters { get; protected set; } = new(); + public string Tag { get; protected set; } + public int? CommandTimeout { get; protected set; } +} + +public class SqlConfiguration : SqlConfiguration where TConfig : SqlConfiguration +{ + public TConfig WithParameters(params object[] parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + Parameters = new HashSet(parameters.Select(ParamValue.Create)); + return (TConfig)this; + } + + public TConfig WithParameters(params ParamValue[] parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + Parameters.UnionWith(parameters); + return (TConfig)this; + } + + public TConfig WithTag(string tag) + { + Tag = tag ?? throw new ArgumentNullException(nameof(tag)); + return (TConfig)this; + } + + public TConfig WithCommandTimeout(int commandTimeout) + { + CommandTimeout = commandTimeout; + return (TConfig)this; + } +} + +public class DefaultSqlConfiguration : SqlConfiguration +{ +} From 00c19d342cf1699899a514fd4059661985fdd522 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 15:42:56 +0100 Subject: [PATCH 24/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(providers):?= =?UTF-8?q?=20adapt=20SqlServer/PostgreSql/MySql/MongoDb=20to=20v5=20shape?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/Set.cs | 61 +------------------ .../Repository/UnitOfWork.cs | 54 ++++++++-------- .../Extensions/SqlConfigurationExtensions.cs | 14 ----- .../Repository/Set.cs | 2 +- .../Extensions/SqlConfigurationExtensions.cs | 14 ----- .../Repository/Set.cs | 2 +- .../Extensions/SqlConfigurationExtensions.cs | 14 ----- .../Repository/Set.cs | 2 +- .../Repository/UnitOfWork.cs | 2 +- .../GetEntityByIdSpecification.cs | 2 +- 10 files changed, 33 insertions(+), 134 deletions(-) delete mode 100644 src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Extensions/SqlConfigurationExtensions.cs delete mode 100644 src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Extensions/SqlConfigurationExtensions.cs delete mode 100644 src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Extensions/SqlConfigurationExtensions.cs diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs index 339d1bd..dbe5b02 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs @@ -1,9 +1,6 @@ using System.Linq.Expressions; using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; @@ -13,7 +10,7 @@ namespace eQuantic.Core.Data.EntityFramework.MongoDb.Repository; -public class Set : SetBase where TEntity : class, IEntity, new() +public class Set : SetBase where TEntity : class, IEntity { private readonly IServiceProvider _serviceProvider; private readonly string? _collectionName; @@ -31,7 +28,7 @@ public Set(IServiceProvider serviceProvider, DbContext context) : base(context) _databaseName = context.GetService() .FindExtension()?.DatabaseName; } - + public override long DeleteMany(Expression> filter) { var result = GetCollection().DeleteMany(filter); @@ -70,58 +67,6 @@ public override async Task UpdateManyAsync(Expression> return result.ModifiedCount; } - public override IQueryable GetQueryable(Action configuration, Func, IQueryable> internalQueryAction) - { - if (configuration == null) - { - return internalQueryAction.Invoke(this); - } - - var config = GetConfig(configuration); - var queryableConfig = config as QueryableConfiguration; - - IQueryable query = this; - - if (config.HasNoTracking) - { - query = query.AsNoTracking(); - } - - if (config.Properties?.Any() == true) - { - query = query.IncludeMany(config.Properties.ToArray()); - } - - if (queryableConfig?.IgnoreQueryFilters == true) - { - query = query.IgnoreQueryFilters(); - } - - if (!string.IsNullOrEmpty(config.Tag)) - { - query = query.TagWith(config.Tag); - } - - if (queryableConfig != null) - { - query = queryableConfig.BeforeCustomization.Invoke(query); - } - - query = internalQueryAction.Invoke(query); - - if (config.SortingColumns.Any()) - { - query = query.OrderBy(config.SortingColumns.ToArray()); - } - - if (queryableConfig != null) - { - query = queryableConfig.AfterCustomization.Invoke(query); - } - - return query; - } - private IMongoCollection GetCollection() { return GetDatabase().GetCollection(_collectionName); @@ -152,4 +97,4 @@ private IMongoDatabase GetDatabase() _mongoDatabase = client.GetDatabase(_databaseName); return _mongoDatabase; } -} \ No newline at end of file +} diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs index d364f68..8e38967 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs @@ -18,7 +18,7 @@ public abstract class UnitOfWork : IQueryableUnitOfWork /// The context /// protected readonly DbContext Context; - + /// /// The disposed /// @@ -34,7 +34,7 @@ protected UnitOfWork(IServiceProvider serviceProvider, DbContext context) ServiceProvider = serviceProvider; Context = context; } - + public int Commit() { return Context.SaveChanges(); @@ -115,16 +115,16 @@ public Task CommitAsync(Action options, CancellationToken canc return CommitAsync(cancellationToken); } - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() + public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity { ((Set)InternalCreateSet()).ApplyCurrentValues(original, current); } - public void Attach(TEntity item) where TEntity : class, IEntity, new() + public void Attach(TEntity item) where TEntity : class, IEntity { ((Set)InternalCreateSet()).Attach(item); } - + public void LoadCollection(TEntity item, Expression>> navigationProperty, Expression>? filter = null) where TEntity : class where TElement : class @@ -179,42 +179,38 @@ public virtual SaveOptions GetSaveOptions() { return new SaveOptions(); } - - public virtual IRepository GetRepository() - where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork + + public virtual IRepository GetRepository() + where TEntity : class, IEntity { - var repo = ServiceProvider.GetRequiredService>(); - return repo; + return ServiceProvider.GetRequiredService>(); } - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork + public IAsyncRepository GetAsyncRepository() + where TEntity : class, IEntity { - return ServiceProvider.GetRequiredService>(); + return ServiceProvider.GetRequiredService>(); } - public Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => InternalCreateSet(); + public Data.Repository.ISet CreateSet() where TEntity : class, IEntity => InternalCreateSet(); - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + public IQueryableRepository GetQueryableRepository() + where TEntity : class, IEntity { - return ServiceProvider.GetRequiredService>(); + return ServiceProvider.GetRequiredService>(); } - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + public IAsyncQueryableRepository GetAsyncQueryableRepository() + where TEntity : class, IEntity { - return ServiceProvider.GetRequiredService>(); + return ServiceProvider.GetRequiredService>(); } - + internal DbContext GetDbContext() => Context; - - internal Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => + + internal Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity => new Set(ServiceProvider, Context); - + /// /// Disposes this instance /// @@ -223,7 +219,7 @@ public void Dispose() Dispose(true); GC.SuppressFinalize(this); } - + /// /// Disposes the disposing /// @@ -246,4 +242,4 @@ protected virtual void Dispose(bool disposing) public abstract class UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : UnitOfWork(serviceProvider, context) - where TDbContext : DbContext; \ No newline at end of file + where TDbContext : DbContext; diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Extensions/SqlConfigurationExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Extensions/SqlConfigurationExtensions.cs deleted file mode 100644 index 08a7235..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Extensions/SqlConfigurationExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using eQuantic.Core.Data.Repository.Config; -using Microsoft.EntityFrameworkCore; - -namespace eQuantic.Core.Data.EntityFramework.MySql.Repository.Extensions; - -public static class SqlConfigurationExtensions -{ - private const int DefaultCommandTimeout = 60; - - public static int GetCommandTimeout(this SqlConfiguration config, DbContext context) - { - return config?.CommandTimeout ?? context?.Database.GetCommandTimeout() ?? DefaultCommandTimeout; - } -} diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs index 6194111..957e526 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs @@ -8,7 +8,7 @@ namespace eQuantic.Core.Data.EntityFramework.MySql.Repository; /// MySQL entity set. The implementation lives in ; this type /// is preserved for source compatibility. /// -public class Set : RelationalSet where TEntity : class, IEntity, new() +public class Set : RelationalSet where TEntity : class, IEntity { public Set(DbContext context) : base(context) { diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Extensions/SqlConfigurationExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Extensions/SqlConfigurationExtensions.cs deleted file mode 100644 index 98ba669..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Extensions/SqlConfigurationExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using eQuantic.Core.Data.Repository.Config; -using Microsoft.EntityFrameworkCore; - -namespace eQuantic.Core.Data.EntityFramework.PostgreSql.Repository.Extensions; - -public static class SqlConfigurationExtensions -{ - private const int DefaultCommandTimeout = 60; - - public static int GetCommandTimeout(this SqlConfiguration config, DbContext context) - { - return config?.CommandTimeout ?? context?.Database.GetCommandTimeout() ?? DefaultCommandTimeout; - } -} diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs index ad125df..5e09900 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs @@ -8,7 +8,7 @@ namespace eQuantic.Core.Data.EntityFramework.PostgreSql.Repository; /// PostgreSQL entity set. The implementation lives in ; this /// type is preserved for source compatibility. /// -public class Set : RelationalSet where TEntity : class, IEntity, new() +public class Set : RelationalSet where TEntity : class, IEntity { public Set(DbContext context) : base(context) { diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Extensions/SqlConfigurationExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Extensions/SqlConfigurationExtensions.cs deleted file mode 100644 index 429b446..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Extensions/SqlConfigurationExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using eQuantic.Core.Data.Repository.Config; -using Microsoft.EntityFrameworkCore; - -namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository.Extensions; - -public static class SqlConfigurationExtensions -{ - private const int DefaultCommandTimeout = 60; - - public static int GetCommandTimeout(this SqlConfiguration config, DbContext context) - { - return config?.CommandTimeout ?? context?.Database.GetCommandTimeout() ?? DefaultCommandTimeout; - } -} diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs index 0f58d0c..88a9aa8 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs @@ -8,7 +8,7 @@ namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository; /// SQL Server entity set. The implementation lives in ; this /// type is preserved for source compatibility. /// -public class Set : RelationalSet where TEntity : class, IEntity, new() +public class Set : RelationalSet where TEntity : class, IEntity { public Set(DbContext context) : base(context) { diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs index 88a70e7..2ce2170 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using eQuantic.Core.Data.EntityFramework.Relational.Repository; -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; using Microsoft.EntityFrameworkCore; namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository; diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs index 2950072..8517415 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs @@ -8,7 +8,7 @@ namespace eQuantic.Core.Data.EntityFramework.SqlServer.Specifications; public class GetEntityByIdSpecification : Specification - where TEntity : class, IEntity, new() + where TEntity : class, IEntity { private readonly TKey _id; private readonly RelationalUnitOfWork _unitOfWork; From 1c6262835cdcd4fd63952689f1ecdcdd7b68a997 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 15:51:59 +0100 Subject: [PATCH 25/32] =?UTF-8?q?=E2=9C=85=20test:=20update=20tests=20to?= =?UTF-8?q?=20eQuantic.Core.Data=20v5=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Repoint the SqlServer.Tests (net10) and MongoDb.Tests (net8) ProjectReferences at the per-major provider csprojs the restructure left them pointing past. - FakeEntity implements IEntity (GetKey/SetKey); FakeQueryableUnitOfWork, FakeRepository and RepositoryDisposalTests move to the two-arg (TEntity, TKey) repository generics and IQueryableUnitOfWork-injected constructors. - ReadRepositoryQueryTests: drop the removed Config using, Product -> IEntity, two-arg repos, Action -> QueryOptions, GetPaged -> PageRequest.Of + PagedResult.Items. - SqlExecutorParameterizationTests: repoint the two usings to the rehomed eQuantic.Core.Data.EntityFramework.Relational.Sql namespace (coverage unchanged). - ServiceCollectionExtensionsTests: base registration is now SQL-agnostic; assert it wires the generic repositories and does NOT register ISqlUnitOfWork (checked by name, without referencing the moved type). --- ....Data.EntityFramework.MongoDb.Tests.csproj | 2 +- .../ReadRepositoryQueryTests.cs | 56 +++++++++++-------- .../SqlExecutorParameterizationTests.cs | 3 +- ...ata.EntityFramework.SqlServer.Tests.csproj | 2 +- .../Fakes/FakeEntity.cs | 8 ++- .../Fakes/FakeQueryableUnitOfWork.cs | 30 +++++----- .../Fakes/FakeRepository.cs | 2 +- .../RepositoryDisposalTests.cs | 6 +- .../ServiceCollectionExtensionsTests.cs | 29 +++++++--- 9 files changed, 82 insertions(+), 56 deletions(-) diff --git a/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj index d9f088c..a4edb3e 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj +++ b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj @@ -21,7 +21,7 @@ - + diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs index 1108576..e18bd7e 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs @@ -4,7 +4,8 @@ using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.EntityFramework.Repository.Read; using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Options; using eQuantic.Linq.Specification; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -12,16 +13,21 @@ namespace eQuantic.Core.Data.EntityFramework.SqlServer.Tests; /// -/// Integration coverage (EF Core InMemory) for the read-repository query fixes: -/// A1 — All/Any with a specification must honour the caller's configuration. +/// Integration coverage (EF Core InMemory) for the read-repository query fixes, ported to the v5 +/// -based read surface: +/// A1 — All/Any must honour the caller's query options. /// A2 — Get must not reject a default-valued key (e.g. 0) as a null argument. /// public class ReadRepositoryQueryTests { - private sealed class Product : eQuantic.Core.Data.Repository.IEntity + private sealed class Product : IEntity { public int Id { get; set; } public string Name { get; set; } = string.Empty; + + public int GetKey() => Id; + + public void SetKey(int key) => Id = key; } private sealed class TestDbContext(DbContextOptions options) : DbContext(options) @@ -43,14 +49,14 @@ private static DefaultUnitOfWork NewUnitOfWork(out TestDbContext context) return new DefaultUnitOfWork(new ServiceCollection().BuildServiceProvider(), context); } - private static QueryableReadRepository NewRepository(out TestDbContext context) + private static QueryableReadRepository NewRepository(out TestDbContext context) => new(NewUnitOfWork(out context)); - private static AsyncQueryableReadRepository NewAsyncRepository(out TestDbContext context) + private static AsyncQueryableReadRepository NewAsyncRepository(out TestDbContext context) => new(NewUnitOfWork(out context)); [Test] - public void All_WithSpecification_HonoursConfiguration() + public void All_WithOptions_HonoursConfiguration() { var repository = NewRepository(out var context); context.Products.AddRange( @@ -58,27 +64,29 @@ public void All_WithSpecification_HonoursConfiguration() new Product { Id = 2, Name = "inactive" }); context.SaveChanges(); - // The "inactive" row does not satisfy the specification, so without the configuration being - // applied All() would evaluate over both rows and return false. The configuration narrows the - // query to the "active" row, so the fixed code returns true. + // The "inactive" row does not satisfy the predicate, so without the options being applied All() + // would evaluate over both rows and return false. The options narrow the query to the "active" + // row, so the fixed code returns true. + var spec = new NameSpecification("active"); var result = repository.All( - new NameSpecification("active"), - cfg => cfg.WithAfterCustomization(q => q.Where(p => p.Name == "active"))); + spec.SatisfiedBy(), + new QueryOptions().WithAfterCustomization(q => q.Where(p => p.Name == "active"))); Assert.That(result, Is.True); } [Test] - public void Any_WithSpecification_HonoursConfiguration() + public void Any_WithOptions_HonoursConfiguration() { var repository = NewRepository(out var context); context.Products.Add(new Product { Id = 1, Name = "active" }); context.SaveChanges(); - // The configuration filters out every row, so Any() must return false once it is applied. + // The options filter out every row, so Any() must return false once they are applied. var result = repository.Any( - new NameSpecification("active"), - cfg => cfg.WithAfterCustomization(q => q.Where(_ => false))); + new QueryOptions() + .Where(new NameSpecification("active")) + .WithAfterCustomization(q => q.Where(_ => false))); Assert.That(result, Is.False); } @@ -110,14 +118,14 @@ public void Get_WithExistingKey_ReturnsEntity() } [Test] - public void Get_WithConfiguration_UsesKeyExpression_ReturnsEntity() + public void Get_WithOptions_UsesKeyExpression_ReturnsEntity() { var repository = NewRepository(out var context); context.Products.Add(new Product { Id = 7, Name = "seven" }); context.SaveChanges(); - // A non-null configuration routes Get through GetFindByKeyExpression instead of DbSet.Find. - var found = repository.Get(7, _ => { }); + // Non-null options route Get through GetFindByKeyExpression instead of DbSet.Find. + var found = repository.Get(7, new QueryOptions()); Assert.That(found, Is.Not.Null); Assert.That(found!.Name, Is.EqualTo("seven")); @@ -147,7 +155,7 @@ public async System.Threading.Tasks.Task GetAllAsync_ReturnsAllEntities() new Product { Id = 3, Name = "c" }); context.SaveChanges(); - var all = await repository.GetAllAsync(System.Threading.CancellationToken.None); + var all = await repository.GetAllAsync(); Assert.That(all.Count(), Is.EqualTo(3)); } @@ -163,7 +171,7 @@ public void GetPaged_WithoutSorting_OrdersByPrimaryKeyDeterministically() new Product { Id = 2, Name = "b" }); context.SaveChanges(); - var firstPage = repository.GetPaged(p => true, 1, 2, null).ToList(); + var firstPage = repository.GetPaged(PageRequest.Of(1, 2)).Items; Assert.That(firstPage.Select(p => p.Id), Is.EqualTo(new[] { 1, 2 })); } @@ -180,8 +188,10 @@ public void GetPaged_WithExplicitOrdering_IsPreserved() // Caller orders descending; the primary-key fallback must NOT override it. var firstPage = repository - .GetPaged(p => true, 1, 2, cfg => cfg.WithAfterCustomization(q => q.OrderByDescending(p => p.Id))) - .ToList(); + .GetPaged( + PageRequest.Of(1, 2), + new QueryOptions().WithAfterCustomization(q => q.OrderByDescending(p => p.Id))) + .Items; Assert.That(firstPage.Select(p => p.Id), Is.EqualTo(new[] { 3, 2 })); } diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs index c06b580..91f2ed0 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs @@ -1,8 +1,7 @@ using System; using eQuantic.Core.Data.EntityFramework.Relational.Repository; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Sql; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj index ce2b8e0..1dfbae9 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj @@ -23,7 +23,7 @@ - + diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeEntity.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeEntity.cs index 1c0e981..6a060d9 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeEntity.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeEntity.cs @@ -4,9 +4,15 @@ namespace eQuantic.Core.Data.EntityFramework.Tests.Fakes; /// /// Minimal entity used to close the generic type parameters of the repositories under test. +/// Implements via GetKey/SetKey as required by the v5 +/// read-repository contracts. /// -internal sealed class FakeEntity : IEntity +internal sealed class FakeEntity : IEntity { public int Id { get; set; } public string? Name { get; set; } + + public int GetKey() => Id; + + public void SetKey(int key) => Id = key; } diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs index fb679d6..7e6723d 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs @@ -7,10 +7,10 @@ namespace eQuantic.Core.Data.EntityFramework.Tests.Fakes; /// -/// A non-relational unit of work: it implements but NOT -/// — mirroring the MongoDb provider. -/// Members throw because the DI-registration tests only inspect the service collection; the fake is -/// never instantiated or resolved. +/// A non-relational unit of work: it implements but not the +/// relational SQL unit-of-work contract — mirroring the MongoDb provider. Members throw because the +/// DI-registration tests only inspect the service collection; the fake is never instantiated or +/// resolved. /// internal sealed class FakeQueryableUnitOfWork : IQueryableUnitOfWork { @@ -36,26 +36,22 @@ public Task CommitAsync(Action options, CancellationToken canc public void RollbackChanges() => throw new NotSupportedException(); - public IRepository GetRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork + public IRepository GetRepository() + where TEntity : class, IEntity => throw new NotSupportedException(); - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork + public IAsyncRepository GetAsyncRepository() + where TEntity : class, IEntity => throw new NotSupportedException(); - public eQuantic.Core.Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() + public eQuantic.Core.Data.Repository.ISet CreateSet() where TEntity : class, IEntity => throw new NotSupportedException(); - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + public IQueryableRepository GetQueryableRepository() + where TEntity : class, IEntity => throw new NotSupportedException(); - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + public IAsyncQueryableRepository GetAsyncQueryableRepository() + where TEntity : class, IEntity => throw new NotSupportedException(); } diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs index fef26d0..2e350b0 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs @@ -7,7 +7,7 @@ namespace eQuantic.Core.Data.EntityFramework.Tests.Fakes; /// something to discover. It inherits the marker IRepository transitively through /// . /// -internal sealed class FakeRepository : QueryableRepository +internal sealed class FakeRepository : QueryableRepository { public FakeRepository(FakeQueryableUnitOfWork unitOfWork) : base(unitOfWork) { diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs index 9066131..82365df 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs @@ -14,7 +14,7 @@ public class RepositoryDisposalTests public void Dispose_AsyncQueryableRepository_DoesNotDisposeInjectedUnitOfWork() { var unitOfWork = new FakeQueryableUnitOfWork(); - var repository = new AsyncQueryableRepository(unitOfWork); + var repository = new AsyncQueryableRepository(unitOfWork); repository.Dispose(); @@ -26,7 +26,7 @@ public void Dispose_AsyncQueryableRepository_DoesNotDisposeInjectedUnitOfWork() public void Dispose_AsyncQueryableRepository_IsIdempotent() { var unitOfWork = new FakeQueryableUnitOfWork(); - var repository = new AsyncQueryableRepository(unitOfWork); + var repository = new AsyncQueryableRepository(unitOfWork); repository.Dispose(); repository.Dispose(); @@ -39,7 +39,7 @@ public void Dispose_QueryableReadRepository_DoesNotDisposeInjectedUnitOfWork() { var unitOfWork = new FakeQueryableUnitOfWork(); var repository = new eQuantic.Core.Data.EntityFramework.Repository.Read - .QueryableReadRepository(unitOfWork); + .QueryableReadRepository(unitOfWork); repository.Dispose(); diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs index d07f20a..3eb2bb1 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs @@ -2,27 +2,42 @@ using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.EntityFramework.Tests.Fakes; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Sql; using Microsoft.Extensions.DependencyInjection; namespace eQuantic.Core.Data.EntityFramework.Tests; /// -/// Guards the fix for the DI-registration defect: a non-relational unit of work must not have -/// registered against it, and -/// must be registered exactly once. +/// Guards the DI-registration behaviour of the base (SQL-agnostic) registration: the generic +/// repositories are wired, is registered exactly once, and no +/// relational SQL unit-of-work contract is registered (that moved to the Relational provider in v5). /// public class ServiceCollectionExtensionsTests { [Test] - public void AddQueryableRepositories_NonSqlUnitOfWork_DoesNotRegisterSqlUnitOfWork() + public void AddQueryableRepositories_WiresGenericRepositories() { var services = new ServiceCollection(); services.AddQueryableRepositories(); - Assert.That(services.Any(d => d.ServiceType == typeof(ISqlUnitOfWork)), Is.False, - "ISqlUnitOfWork must not be registered for a unit of work that does not implement it."); + Assert.That(services.Any(d => d.ServiceType == typeof(IQueryableRepository<,>)), Is.True, + "The open-generic IQueryableRepository must be registered by the base registration."); + Assert.That(services.Any(d => d.ServiceType == typeof(IAsyncQueryableRepository<,>)), Is.True, + "The open-generic IAsyncQueryableRepository must be registered by the base registration."); + } + + [Test] + public void AddQueryableRepositories_IsSqlAgnostic_DoesNotRegisterSqlUnitOfWork() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + // ISqlUnitOfWork moved out of eQuantic.Core.Data into the Relational provider in v5, so the base + // registration is SQL-agnostic and must not register it. Checked by name so this base test does + // not reference the moved type. + Assert.That(services.Any(d => d.ServiceType.Name == "ISqlUnitOfWork"), Is.False, + "The base registration must not register ISqlUnitOfWork (it moved to the Relational provider)."); } [Test] From 89a1881a938afdf1308f6d608d990958e975a6c2 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 15:55:24 +0100 Subject: [PATCH 26/32] =?UTF-8?q?=F0=9F=91=B7=20ci:=20prune=20build=20matr?= =?UTF-8?q?ix=20to=20the=20net8/net10=20package=20set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both ci.yml and release.yml matrices dropped the removed net6/net7/net9 variants and the retired multi-target provider csprojs, and picked up the renamed MongoDb.Net8. Now 12 csprojs: base (multi-fw + Net8 + Net10), Relational (multi-fw), and Net8/Net10 for each of SqlServer/PostgreSql/MySql/MongoDb. --- .github/workflows/ci.yml | 14 +------------- .github/workflows/release.yml | 14 +------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6445338..ca473fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,28 +17,16 @@ jobs: matrix: project: - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj - - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj - - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj - - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj - src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj - - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj - - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj - - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj - - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj - - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj - - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj - - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj - - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj - - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj - - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 609fd72..e7d7af9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,28 +21,16 @@ jobs: matrix: project: - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj - - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj - - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj - - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj - src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj - - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj - - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj - - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj - - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj - - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj - - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj - - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj - - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj - - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj - - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj steps: - uses: actions/checkout@v4 From 944aae0bd8130c2dcbd06239d964694736a519b0 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 16:38:36 +0100 Subject: [PATCH 27/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20rewrite=20README?= =?UTF-8?q?=20for=20v5=20contracts=20and=20the=20per-major/multi-framework?= =?UTF-8?q?=20versioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 119 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 44faac1..690b10e 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,107 @@ -# eQuantic Core Data Entity Framework Library +# eQuantic.Core.Data.EntityFramework -The **eQuantic Data Core** provides a robust implementation of the **Repository Pattern** specifically for **Entity Framework Core**. +**The Entity Framework Core implementation of [eQuantic.Core.Data](https://github.com/eQuantic/core-data).** +You code against the provider-agnostic `IRepository` / `IUnitOfWork` contracts; this package +supplies the EF Core engine for **SQL Server, PostgreSQL, MySQL and MongoDB**. -This library offers seamless integration with the following database providers: +```csharp +// A repository over any IEntity, obtained from your DbContext-backed unit of work: +var repo = unitOfWork.GetAsyncRepository(); -- **SQL Server** -- **PostgreSQL** -- **MySQL** -- **MongoDB** (via EF Core provider) +// Query typed and fluent — one QueryOptions, no magic strings: +var page = await repo.GetPagedAsync( + PageRequest.Of(pageIndex: 1, pageSize: 20), + new QueryOptions() + .Where(o => o.Total, FilterOperator.GreaterThan, 100m) + .And(o => o.Customer.Name, FilterOperator.Contains, term) + .OrderByDescending(o => o.CreatedAt) + .Include(nameof(OrderData.Customer)) + .NoTracking()); -## Version 4.4.0 +// page is a PagedResult: Items + TotalCount + PageIndex/PageSize/PageCount + Has*Page +``` + +## What this package gives you -### Key Features and Improvements (v4.4.0) +`eQuantic.Core.Data` defines the **contracts** — `IRepository`, `IUnitOfWork`, `QueryOptions`, +`PageRequest`, `PagedResult`, specifications — with the persistence engine kept out of the type signatures +(`IRepository`, not `IRepository`). -- **.NET 10 Support**: Full compatibility with .NET 10, including optimized `ExecuteUpdate` operations using the new `UpdateSettersBuilder`. -- **Improved Expression Conversion**: Enhanced reflection-based method lookup for `ExecuteUpdate` setters, ensuring robustness across different .NET frameworks and provider-specific quirks. -- **Optimized Resource Management**: Implemented internal cleanup mechanisms in `UnitOfWork` to better manage memory and database connections. -- **Enhanced Data Integrity**: Fixed shadow field inheritance issues by replacing brittle `new` keyword usage with `internal virtual` properties. -- **Strict Pagination Validation**: Added explicit validation for pagination parameters in `QueryableReadRepository`. -- **Full Multi-Provider Support**: Optimized implementations for **SqlServer**, **PostgreSql**, **MySql**, and **MongoDb**. +This package is the **Entity Framework Core implementation** of those contracts. It translates a single +`QueryOptions` into an EF `IQueryable` — applying, in order, custom *before* hooks, the +specification and predicate filter, eager `Include`s, sortings (server-side / EF-translatable), +`AsNoTracking`, `IgnoreQueryFilters`, query tags and custom *after* hooks — and returns `PagedResult` +from paged reads. The relational providers also carry a parameterized raw-SQL executor (functions and +stored procedures, always via `DbParameter`). -## Installation +## How you query -To install **eQuantic.Core.Data.EntityFramework**, run the following command in the [Package Manager Console](https://docs.nuget.org/docs/start-here/using-the-package-manager-console): +`QueryOptions` mirrors the eQuantic.Linq query builders, so filters read like code and fail at +compile time — not at runtime: -```powershell -Install-Package eQuantic.Core.Data.EntityFramework +```csharp +new QueryOptions() + .Where(o => o.Total, FilterOperator.GreaterThanOrEqual, 100m) // typed member selector + .And(o => o.Status, FilterOperator.Equal, OrderStatus.Paid) // clauses fold left to right: + .Or(o => o.Customer.IsVip, FilterOperator.Equal, true) // (total>=100 AND paid) OR vip + .OrderByDescending(o => o.CreatedAt) + .ThenBy("customer.name") // string path for dynamic columns + .Include(nameof(OrderData.Customer)) + .NoTracking(); ``` -For specific providers, install the corresponding package: +You reach for whichever filter form fits — member selector, `string` path, `ISpecification`, +`Expression>`, a serialized `ExpressionModel`, or an `eQuantic.Linq.Web` query string — all +end up as one predicate the provider translates. The full query-string grammar is documented in the +[eQuantic.Linq reference](https://github.com/eQuantic/core-linq/blob/main/docs/query-string-syntax.md). + +## Providers + +| Package | Database | +|---|---| +| `eQuantic.Core.Data.EntityFramework.SqlServer` | SQL Server | +| `eQuantic.Core.Data.EntityFramework.PostgreSql` | PostgreSQL | +| `eQuantic.Core.Data.EntityFramework.MySql` | MySQL (Pomelo) | +| `eQuantic.Core.Data.EntityFramework.MongoDb` | MongoDB (EF Core provider) | + +The three relational providers share `eQuantic.Core.Data.EntityFramework.Relational`; every provider builds +on the base `eQuantic.Core.Data.EntityFramework`. Register your `DbContext`-backed unit of work and the +open-generic repositories through `AddRelationalRepositories()` — the +full wiring is in the [walkthrough](Repository.md). + +## Versioning — pick the package major that matches your runtime + +This library targets **.NET 8** and **.NET 10**, and each runtime is published as its **own package major** +so the EF Core lines never mix: + +| Your app | Install | Targets | +|---|---|---| +| .NET 8 | **8.x** (e.g. `8.2.0`) | `net8.0`, EF Core 8 | +| .NET 10 | **10.x** (e.g. `10.1.0`) | `net10.0`, EF Core 10 | + +> The shared multi-framework assemblies (the referenceable base `eQuantic.Core.Data.EntityFramework` and +> `eQuantic.Core.Data.EntityFramework.Relational`) stay in the **4.x** line on purpose — a neutral lane that +> must not be read as a .NET version. You normally consume only the provider package for your runtime +> (8.x / 10.x), which pulls the right shared assemblies transitively. + +## Install + +```bash +# .NET 8 app + SQL Server +dotnet add package eQuantic.Core.Data.EntityFramework.SqlServer --version 8.* + +# .NET 10 app + PostgreSQL +dotnet add package eQuantic.Core.Data.EntityFramework.PostgreSql --version 10.* +``` -- `eQuantic.Core.Data.EntityFramework.SqlServer` -- `eQuantic.Core.Data.EntityFramework.PostgreSql` -- `eQuantic.Core.Data.EntityFramework.MySql` -- `eQuantic.Core.Data.EntityFramework.MongoDb` +Swap the suffix for `PostgreSql`, `MySql` or `MongoDb` as needed. -## Usage Examples +## Learn more -The following are examples of implementing the repository pattern: +- [Repository Pattern walkthrough](Repository.md) — data entities, unit of work, repository and + specifications, end to end. +- [eQuantic.Core.Data](https://github.com/eQuantic/core-data) — the contracts and the `QueryOptions` / + `PagedResult` / `PageRequest` query surface, backed by the + [eQuantic.Linq](https://github.com/eQuantic/core-linq) query engine. -- [Repository Pattern Implementation](Repository.md) +MIT © eQuantic Tech From 7b97d918d3fe6e6a94f23696cda228e2c69b6364 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 16:50:19 +0100 Subject: [PATCH 28/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20rewrite=20Reposito?= =?UTF-8?q?ry.md=20walkthrough=20for=20eQuantic.Core.Data=20v5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the v4 walkthrough (IEntity without key, PagedList, Get(id, lambda), GetPaged(spec, pageIndex, pageSize, ...)) with a faithful v5 end-to-end slice: IEntity with GetKey/SetKey, UnitOfWork + AddRelationalRepositories, GetAsyncQueryableRepository, a single QueryOptions (typed Where/And/Or, OrderBy, Include, NoTracking), PagedResult, specifications, set-based UpdateMany/DeleteMany, custom repositories, and a domain service. Every snippet verified against the v5 contracts and the provider source. --- Repository.md | 459 +++++++++++++++++++++++++++++--------------------- 1 file changed, 265 insertions(+), 194 deletions(-) diff --git a/Repository.md b/Repository.md index 440a036..b9230c7 100644 --- a/Repository.md +++ b/Repository.md @@ -1,263 +1,334 @@ -# Repository Pattern with Entity Framework +# Repository walkthrough (Entity Framework Core) -## UnitOfWork example: +An end-to-end slice built on **eQuantic.Core.Data v5** and this EF Core provider: data entities → a +unit of work → repositories → specifications → a domain service that consumes them. You code against the +provider-agnostic contracts (`IEntity`, `IQueryableUnitOfWork`, `QueryOptions`, +`PageRequest`/`PagedResult`); the provider supplies the EF Core engine. The examples use SQL Server — +PostgreSQL and MySQL are identical (swap the provider namespace and `UseSqlServer`), and MongoDB differs +only in registration (see §3). + +## 1. Data entities — `IEntity` + +An entity is a plain class that implements `IEntity`. The key is exposed through `GetKey()`/ +`SetKey()` (there is no mandated `Id` property, though you will usually have one). + +```csharp +using System; +using eQuantic.Core.Data.Repository; + +public enum OrderStatus { Pending, Paid, Cancelled } + +public class CustomerData : IEntity +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public bool IsVip { get; set; } + + public Guid GetKey() => Id; + public void SetKey(Guid key) => Id = key; +} + +public class OrderData : IEntity +{ + public Guid Id { get; set; } + public decimal Total { get; set; } + public OrderStatus Status { get; set; } + public DateTime CreatedAt { get; set; } + + public Guid CustomerId { get; set; } + public CustomerData Customer { get; set; } = default!; + + public Guid GetKey() => Id; + public void SetKey(Guid key) => Id = key; +} +``` + +## 2. The DbContext + +A regular EF Core `DbContext` — the provider works with whatever context you already have. ```csharp -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Ioc; using Microsoft.EntityFrameworkCore; -namespace eQuantic.Core.Web.Examples.Infrastructure +public class AppDbContext(DbContextOptions options) : DbContext(options) { - public class ExampleUnitOfWork : UnitOfWork - { - private readonly IContainer _container; - - public ExampleUnitOfWork(IContainer container, DbContext context) : base(context) - { - _container = container; - } - - public override TRepository GetRepository() - { - return _container.Resolve(); - } - - public override TRepository GetRepository(string name) - { - return _container.Resolve(name); - } - } + public DbSet Orders => Set(); + public DbSet Customers => Set(); } ``` -## Entity data example: +## 3. The unit of work + +Derive the provider's `UnitOfWork`. Declaring your own interface (deriving +`IQueryableUnitOfWork`) keeps call sites decoupled from the concrete type. ```csharp using System; +using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; // provider base using eQuantic.Core.Data.Repository; -namespace eQuantic.Core.Web.Examples.Infrastructure.Data +public interface IAppUnitOfWork : IQueryableUnitOfWork { - public class UserData : IEntity - { - public Guid Id { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public string Email { get; set; } - } } -namespace eQuantic.Core.Web.Examples.Infrastructure.Data +public class AppUnitOfWork : UnitOfWork, IAppUnitOfWork { - public class PersonData : IEntity + public AppUnitOfWork(IServiceProvider serviceProvider, AppDbContext context) + : base(serviceProvider, context) { - public Guid Id { get; set; } - public string Name { get; set; } - public DateTime BirthDate { get; set; } - public string Phone { get; set; } - public virtual UserData User { get; set; } } } ``` -## Repository example: +Register the context and the repositories. `AddRelationalRepositories` wires the unit of work, the generic +repositories, and — because the relational unit of work implements it — the raw-SQL +`ISqlUnitOfWork`. -### Contract +```csharp +using eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +services.AddDbContext(o => o.UseSqlServer(connectionString)); +services.AddRelationalRepositories(); +``` + +No custom unit-of-work type? Use the provider's `DefaultUnitOfWork` (over a bare `DbContext`) instead. +MongoDB has no SQL executor, so its unit of work derives +`eQuantic.Core.Data.EntityFramework.MongoDb.Repository.UnitOfWork` and is registered with the +SQL-agnostic `AddQueryableRepositories()`. + +## 4. Getting a repository + +Ask the unit of work for a repository over any `IEntity`. The queryable accessors resolve the +generic repositories registered above: + +```csharp +// asynchronous read + write: +IAsyncQueryableRepository orders = + unitOfWork.GetAsyncQueryableRepository(); + +// synchronous sibling: +IQueryableRepository ordersSync = + unitOfWork.GetQueryableRepository(); +``` + +`GetAsyncRepository()` / `GetRepository()` return the `IAsyncRepository`/ +`IRepository` shapes and are served by **custom** repositories (§8). + +## 5. Reading — one `QueryOptions` + +All query shaping — filtering, includes, sorting, tracking — is expressed through a single +`QueryOptions`. Filters read like code and fail at compile time; clauses fold left to right. + +```csharp +using eQuantic.Core.Data.Repository.Options; +using eQuantic.Linq.Web; // FilterOperator + +var options = new QueryOptions() + .Where(o => o.Total, FilterOperator.GreaterThanOrEqual, 100m) // typed member selector + .And(o => o.Status, FilterOperator.Equal, OrderStatus.Paid) // (total>=100 AND paid) + .Or(o => o.Customer.IsVip, FilterOperator.Equal, true) // OR the customer is VIP + .OrderByDescending(o => o.CreatedAt) + .ThenBy("customer.name") // string path for dynamic columns + .Include(nameof(OrderData.Customer)) // eager load + .NoTracking(); +``` + +Paged reads return a `PagedResult` — the items plus the totals needed to render a pager: ```csharp -using System; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Web.Examples.Infrastructure.Data; -namespace eQuantic.Core.Web.Examples.Infrastructure.Repositories.Contracts -{ - public interface IPersonRepository : IAsyncRepository - { - } -} +PagedResult page = await orders.GetPagedAsync(PageRequest.Of(pageIndex: 1, pageSize: 20), options); + +foreach (var order in page.Items) { /* ... */ } +_ = page.TotalCount; // total across all pages +_ = page.PageIndex; _ = page.PageSize; // echoed back +_ = page.PageCount; // computed +_ = page.HasPreviousPage; _ = page.HasNextPage; ``` -### Implementation +The rest of the read surface follows the same shape — pass a `QueryOptions` (or none): ```csharp -using System; -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Web.Examples.Infrastructure.Data; -using eQuantic.Core.Web.Examples.Infrastructure.Repositories.Contracts; +// by key (a non-null options routes through the key predicate + includes; otherwise DbSet.Find): +OrderData? one = await orders.GetAsync(id, new QueryOptions().Include(nameof(OrderData.Customer))); -namespace eQuantic.Core.Web.Examples.Infrastructure.Repositories -{ - public class PersonRepository : AsyncRepository, IPersonRepository - { - public PersonRepository(ExampleUnitOfWork unitOfWork) : base(unitOfWork) - { - } - } -} +// by predicate: +IEnumerable big = await orders.GetFilteredAsync(o => o.Total >= 100m, new QueryOptions().NoTracking()); + +// projection (server-side Select): +IEnumerable summaries = await orders.GetMappedAsync( + o => new OrderSummary(o.Id, o.Total, o.Customer.Name), + new QueryOptions().Include(nameof(OrderData.Customer))); + +// paged projection: +PagedResult summaryPage = await orders.GetPagedAsync( + PageRequest.Of(1, 20), + o => new OrderSummary(o.Id, o.Total, o.Customer.Name), + new QueryOptions().Include(nameof(OrderData.Customer))); + +// aggregates (Count returns long; Sum has an overload per numeric type): +long paidCount = await orders.CountAsync(new QueryOptions().Where(o => o.Status, FilterOperator.Equal, OrderStatus.Paid)); +decimal paidTotal = await orders.SumAsync(o => o.Total, new QueryOptions().Where(o => o.Status, FilterOperator.Equal, OrderStatus.Paid)); + +public record OrderSummary(Guid Id, decimal Total, string Customer); ``` -# DDD Pattern +Every method has a synchronous twin on the queryable repository (`GetPaged`, `Get`, `Count`, `Sum`, …). -## Domain Entity example: +## 6. Specifications + +Encapsulate a reusable rule as an `ISpecification` (base class `Specification`). ```csharp using System; +using System.Linq.Expressions; +using eQuantic.Linq.Specification; -namespace eQuantic.Core.Web.Examples.Domain.Entities +public sealed class PaidOrdersSpecification : Specification { - public class User - { - public ShortGuid Id { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public string Email { get; set; } - } + public override Expression> SatisfiedBy() => o => o.Status == OrderStatus.Paid; } +``` -namespace eQuantic.Core.Web.Examples.Domain.Entities -{ - public class Person - { - public ShortGuid Id { get; set; } - public string Name { get; set; } - public DateTime BirthDate { get; set; } - public string Phone { get; set; } - public virtual User User { get; set; } - } -} +Apply it either as the filter inside `QueryOptions`, or directly through `AllMatchingAsync`: + +```csharp +var spec = new PaidOrdersSpecification(); + +// inside QueryOptions (composes with sorting, includes, tracking, ...): +var recentPaid = await orders.GetPagedAsync( + PageRequest.Of(1, 20), + new QueryOptions().Where(spec).OrderByDescending(o => o.CreatedAt)); + +// or directly: +IEnumerable allPaid = await orders.AllMatchingAsync(spec, new QueryOptions().NoTracking()); ``` -## Specification Pattern +## 7. Writing + +Writes are staged on the repository and persisted when the unit of work commits. ```csharp -using System; -using System.Linq.Expressions; -using eQuantic.Core.Web.Examples.Infrastructure.Data; -using eQuantic.Core.Linq.Specification; +var order = new OrderData { Id = Guid.NewGuid(), Total = 150m, Status = OrderStatus.Pending, CreatedAt = DateTime.UtcNow }; -namespace eQuantic.Core.Web.Examples.Domain.Specification -{ - public class PersonSpecification : Specification - { - private readonly string _term; - - public PersonSpecification(string term) - { - _term = term; - } - public override Expression> SatisfiedBy() - { - return p => p.Name.StartsWith(_term) || p.User.UserName.StartsWith(_term) || p.User.Email.StartsWith(_term); - } - } -} +await orders.AddAsync(order); // stage an insert +await orders.AddRangeAsync(new[] { order1, order2 }); // stage several + +order.Status = OrderStatus.Paid; +await orders.ModifyAsync(order); // mark modified + +await orders.RemoveAsync(order); // stage a delete + +int affected = await unitOfWork.CommitAsync(); // one round-trip, returns affected rows ``` -## Domain Services +Set-based writes run directly in the database (EF `ExecuteUpdate`/`ExecuteDelete`) and do not need a +commit: + +```csharp +long cancelled = await orders.UpdateManyAsync( + o => o.Status == OrderStatus.Pending, + o => new OrderData { Status = OrderStatus.Cancelled }); + +long removed = await orders.DeleteManyAsync(o => o.Total == 0m); +``` -### Contract +`DeleteManyAsync`/`UpdateManyAsync` also accept an `ISpecification` in place of the predicate. + +## 8. Custom repositories + +Need repository-specific methods, or the plain `IRepository`/`IAsyncRepository` shape resolved by +`GetRepository`/`GetAsyncRepository`? Declare an interface and derive the generic base: ```csharp -using System; -using eQuantic.Core.Collections; -using eQuantic.Core.Web.Examples.Domain.Entities; +using eQuantic.Core.Data.EntityFramework.Repository; +using eQuantic.Core.Data.Repository; + +public interface IOrderRepository : IRepository, IAsyncRepository +{ +} -namespace eQuantic.Core.Web.Examples.Domain.Services.Contracts +public class OrderRepository : AsyncQueryableRepository, IOrderRepository { - public interface IPersonService + public OrderRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { - Person Get(Guid id); - bool Create(Person person); - bool Update(Person person); - bool Delete(Guid id); - PagedList Find(string term, int pageIndex, int pageSize); } } ``` -### Implementation +Register by scanning the assembly, then resolve through the unit of work (or inject `IOrderRepository` +directly): + +```csharp +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; + +services.AddDbContext(o => o.UseSqlServer(connectionString)); +services.AddCustomRepositories(o => o + .AddLifetime(ServiceLifetime.Scoped) + .FromAssembly(typeof(OrderRepository).Assembly)); + +// later: +IAsyncRepository repo = unitOfWork.GetAsyncRepository(); +IRepository repoSync = unitOfWork.GetRepository(); +``` + +## 9. A domain service + +Putting it together — a service consumes the unit of work and its repositories, keeping EF Core out of the +domain layer. ```csharp using System; -using System.Collections.Generic; -using AutoMapper; -using eQuantic.Core.Collections; -using eQuantic.Core.Linq; -using eQuantic.Core.Web.Examples.Domain.Entities; -using eQuantic.Core.Web.Examples.Domain.Specification; -using eQuantic.Core.Web.Examples.Infrastructure; -using eQuantic.Core.Web.Examples.Infrastructure.Data; -using eQuantic.Core.Web.Examples.Infrastructure.Repositories.Contracts; - -namespace eQuantic.Core.Web.Examples.Domain.Services +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Options; +using eQuantic.Linq.Web; + +public class OrderService { - public class PersonService : IPersonService - { - public IMapper Mapper { get; } - public ExampleUnitOfWork UnitOfWork { get; } - - public PersonService(IMapper mapper, ExampleUnitOfWork unitOfWork) - { - Mapper = mapper; - UnitOfWork = unitOfWork; - } - - public Person Get(Guid id) - { - Person person = null; - var repo = UnitOfWork.GetRepository(); - var item = repo.Get(id, p => p.User); - if (item != null) person = Mapper.Map(item); - - return person; - } - - public bool Create(Person person) - { - var repo = UnitOfWork.GetRepository(); - var item = Mapper.Map(person); - repo.Add(item); - return UnitOfWork.Commit() > 0; - } - - public bool Update(Person person) - { - var repo = UnitOfWork.GetRepository(); - var item = repo.Get(person.Id); - Mapper.Map(person, item); - repo.Modify(item); - return UnitOfWork.Commit() > 0; - } - - public bool Delete(Guid id) - { - var repo = UnitOfWork.GetRepository(); - var item = repo.Get(id); - repo.Remove(item); - return UnitOfWork.Commit() > 0; - } - - public PagedList Find(string term, int pageIndex, int pageSize) - { - var repo = UnitOfWork.GetRepository(); - var specification = new PersonSpecification(term); - var count = repo.Count(specification); - var items = repo.GetPaged(specification, pageIndex, pageSize, - new[] {new Sorting {Column = c => c.Name}}, p => p.User); - var persons = Mapper.Map>(items); - return new PagedList(persons, count){ PageIndex = pageIndex, PageSize = pageSize}; - } - } -} -``` + private readonly IAppUnitOfWork _unitOfWork; -## Advanced Features (v4.4.0) + public OrderService(IAppUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; -### .NET 10 ExecuteUpdate Support + public Task GetAsync(Guid id, CancellationToken ct = default) => + _unitOfWork.GetAsyncQueryableRepository() + .GetAsync(id, new QueryOptions().Include(nameof(OrderData.Customer)), ct); -Starting with version 4.4.0, the repository fully supports .NET 10's `ExecuteUpdate` with the new `UpdateSettersBuilder` syntax. The `ExpressionConverter` automatically handles the transformation of anonymous object or member init expressions into the optimized EF Core format. + public async Task CreateAsync(OrderData order, CancellationToken ct = default) + { + var repo = _unitOfWork.GetAsyncQueryableRepository(); + await repo.AddAsync(order, ct); + await _unitOfWork.CommitAsync(ct); + return order.Id; + } -### Resource Management + public async Task DeleteAsync(Guid id, CancellationToken ct = default) + { + var repo = _unitOfWork.GetAsyncQueryableRepository(); + var order = await repo.GetAsync(id, cancellationToken: ct); + if (order is null) return false; -The `UnitOfWork` implementation now includes internal resource tracking to ensure that database connections and related resources are properly disposed of, even when multiple repositories are shared within the same scope. + await repo.RemoveAsync(order); + return await _unitOfWork.CommitAsync(ct) > 0; + } + + public Task> FindAsync(decimal minTotal, int pageIndex, int pageSize, CancellationToken ct = default) => + _unitOfWork.GetAsyncQueryableRepository() + .GetPagedAsync( + PageRequest.Of(pageIndex, pageSize), + new QueryOptions() + .Where(o => o.Total, FilterOperator.GreaterThanOrEqual, minTotal) + .OrderByDescending(o => o.CreatedAt) + .Include(nameof(OrderData.Customer)) + .NoTracking(), + ct); + + public Task RevenueAsync(CancellationToken ct = default) => + _unitOfWork.GetAsyncQueryableRepository() + .SumAsync(o => o.Total, new QueryOptions().Where(o => o.Status, FilterOperator.Equal, OrderStatus.Paid), ct); +} +``` From bbb2b243310920df6fc18e098fc940fa027c7690 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 16:59:49 +0100 Subject: [PATCH 29/32] =?UTF-8?q?=F0=9F=90=9B=20fix:=20serve=20IRepository?= =?UTF-8?q?/IAsyncRepository=20from=20the=20generic=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IUnitOfWork.GetRepository / GetAsyncRepository (the contract's headline accessors, used in core-data's own README) return IRepository<,> / IAsyncRepository<,>, but the generic registration only wired the sibling IQueryableRepository<,> / IAsyncQueryableRepository<,> — so those accessors threw at runtime under AddQueryableRepositories/AddRelationalRepositories. The concrete QueryableRepository/AsyncQueryableRepository already satisfy the plain interfaces structurally (the queryable read interfaces extend the plain ones), so they now also declare them and the generic registration serves all four. Adds a regression test and aligns the walkthrough to the family-standard accessors. --- Repository.md | 24 ++++++++++--------- .../Repository/AsyncQueryableRepository.cs | 4 +++- .../Extensions/ServiceCollectionExtensions.cs | 2 ++ .../Repository/QueryableRepository.cs | 3 ++- .../ServiceCollectionExtensionsTests.cs | 17 +++++++++++++ 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/Repository.md b/Repository.md index b9230c7..93c91b4 100644 --- a/Repository.md +++ b/Repository.md @@ -105,16 +105,18 @@ generic repositories registered above: ```csharp // asynchronous read + write: -IAsyncQueryableRepository orders = - unitOfWork.GetAsyncQueryableRepository(); +IAsyncRepository orders = + unitOfWork.GetAsyncRepository(); // synchronous sibling: -IQueryableRepository ordersSync = - unitOfWork.GetQueryableRepository(); +IRepository ordersSync = + unitOfWork.GetRepository(); ``` -`GetAsyncRepository()` / `GetRepository()` return the `IAsyncRepository`/ -`IRepository` shapes and are served by **custom** repositories (§8). +All four accessors resolve from the generic registration above — the plain +`GetAsyncRepository`/`GetRepository` shown here and the richer +`GetAsyncQueryableRepository`/`GetQueryableRepository` variants (which add the `IQueryable`/`ISet` surface). +Custom repositories (§8) layer your own named interface on top of the same wiring. ## 5. Reading — one `QueryOptions` @@ -295,12 +297,12 @@ public class OrderService public OrderService(IAppUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; public Task GetAsync(Guid id, CancellationToken ct = default) => - _unitOfWork.GetAsyncQueryableRepository() + _unitOfWork.GetAsyncRepository() .GetAsync(id, new QueryOptions().Include(nameof(OrderData.Customer)), ct); public async Task CreateAsync(OrderData order, CancellationToken ct = default) { - var repo = _unitOfWork.GetAsyncQueryableRepository(); + var repo = _unitOfWork.GetAsyncRepository(); await repo.AddAsync(order, ct); await _unitOfWork.CommitAsync(ct); return order.Id; @@ -308,7 +310,7 @@ public class OrderService public async Task DeleteAsync(Guid id, CancellationToken ct = default) { - var repo = _unitOfWork.GetAsyncQueryableRepository(); + var repo = _unitOfWork.GetAsyncRepository(); var order = await repo.GetAsync(id, cancellationToken: ct); if (order is null) return false; @@ -317,7 +319,7 @@ public class OrderService } public Task> FindAsync(decimal minTotal, int pageIndex, int pageSize, CancellationToken ct = default) => - _unitOfWork.GetAsyncQueryableRepository() + _unitOfWork.GetAsyncRepository() .GetPagedAsync( PageRequest.Of(pageIndex, pageSize), new QueryOptions() @@ -328,7 +330,7 @@ public class OrderService ct); public Task RevenueAsync(CancellationToken ct = default) => - _unitOfWork.GetAsyncQueryableRepository() + _unitOfWork.GetAsyncRepository() .SumAsync(o => o.Total, new QueryOptions().Where(o => o.Status, FilterOperator.Equal, OrderStatus.Paid), ct); } ``` diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs index dd92dd8..b034016 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs @@ -15,7 +15,9 @@ namespace eQuantic.Core.Data.EntityFramework.Repository; public class AsyncQueryableRepository : AsyncQueryableReadRepository, IAsyncQueryableRepository, - IQueryableRepository + IQueryableRepository, + IAsyncRepository, + IRepository where TEntity : class, IEntity { private readonly AsyncWriteRepository _asyncWriteRepository; diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs index 4f380cc..878172b 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs @@ -75,7 +75,9 @@ private static void AddUnitOfWork(IServic private static void AddGenericRepositories(IServiceCollection services, ServiceLifetime lifetime) { + services.TryAdd(new ServiceDescriptor(typeof(IRepository<,>), typeof(QueryableRepository<,>), lifetime)); services.TryAdd(new ServiceDescriptor(typeof(IQueryableRepository<,>), typeof(QueryableRepository<,>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IAsyncRepository<,>), typeof(AsyncQueryableRepository<,>), lifetime)); services.TryAdd(new ServiceDescriptor(typeof(IAsyncQueryableRepository<,>), typeof(AsyncQueryableRepository<,>), lifetime)); } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs index a05eb4a..7345467 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs @@ -13,7 +13,8 @@ namespace eQuantic.Core.Data.EntityFramework.Repository; [ExcludeFromCodeCoverage] public class QueryableRepository : QueryableReadRepository, - IQueryableRepository + IQueryableRepository, + IRepository where TEntity : class, IEntity { private readonly IWriteRepository _writeRepository; diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs index 3eb2bb1..75a129e 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs @@ -26,6 +26,23 @@ public void AddQueryableRepositories_WiresGenericRepositories() "The open-generic IAsyncQueryableRepository must be registered by the base registration."); } + [Test] + public void AddQueryableRepositories_WiresPlainRepositories_SoGetRepositoryResolves() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + // IUnitOfWork.GetRepository / GetAsyncRepository return IRepository<,> / IAsyncRepository<,> + // (the plain siblings of the queryable interfaces). The generic registration must also serve + // these — the concrete queryable repositories implement the plain interfaces too — otherwise the + // contract's headline accessors throw at runtime. + Assert.That(services.Any(d => d.ServiceType == typeof(IRepository<,>)), Is.True, + "The open-generic IRepository must be registered so GetRepository resolves."); + Assert.That(services.Any(d => d.ServiceType == typeof(IAsyncRepository<,>)), Is.True, + "The open-generic IAsyncRepository must be registered so GetAsyncRepository resolves."); + } + [Test] public void AddQueryableRepositories_IsSqlAgnostic_DoesNotRegisterSqlUnitOfWork() { From 50f4e4dcd63367646fb95f986081f0689ec23609 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 17:26:45 +0100 Subject: [PATCH 30/32] =?UTF-8?q?=F0=9F=91=B7=20ci:=20least-privilege=20to?= =?UTF-8?q?ken=20permissions=20and=20env-passed=20NuGet=20secret?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the CodeQL "workflow does not contain permissions" findings by adding a top-level 'permissions: contents: read' to ci.yml and release.yml, and the SonarCloud "secrets should not be directly expanded in run steps" finding by passing nuget_key to the push step via env instead of inline ${{ secrets }} expansion. Restores the Security Rating on new code to A. --- .github/workflows/ci.yml | 3 +++ .github/workflows/release.yml | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca473fa..f19a70f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: build: name: Build ${{ matrix.project }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7d7af9..53dc059 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,9 @@ concurrency: group: release-${{ github.ref }} cancel-in-progress: false +permissions: + contents: read + jobs: build: name: Build ${{ matrix.project }} @@ -100,4 +103,6 @@ jobs: path: artifacts merge-multiple: true - name: Push to NuGet.org - run: dotnet nuget push "artifacts/*.nupkg" --skip-duplicate -k ${{ secrets.nuget_key }} -s https://api.nuget.org/v3/index.json + env: + NUGET_KEY: ${{ secrets.nuget_key }} + run: dotnet nuget push "artifacts/*.nupkg" --skip-duplicate -k "$NUGET_KEY" -s https://api.nuget.org/v3/index.json From 765ffcad66e4ae16ded753fa3c3a3c96804929c9 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 17:38:04 +0100 Subject: [PATCH 31/32] =?UTF-8?q?=F0=9F=94=A7=20chore(deps):=20bump=20EF?= =?UTF-8?q?=20Core=20(8.0.29/10.0.10)=20and=20MongoDB.EntityFrameworkCore?= =?UTF-8?q?=20(8.4.2/10.0.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps MongoDB.EntityFrameworkCore to pull a MongoDB.Driver free of the transitive SharpCompress 0.30.1 (NU1902, moderate) and Snappier 1.0.0 (NU1903, high) vulnerabilities flagged by the scanners; the newer MongoDB provider requires a newer EF Core, so Microsoft.EntityFrameworkCore.* is bumped to the latest patch (8.0.29 / 10.0.10) consistently across all packages. Test projects' InMemory + Microsoft.Extensions.DependencyInjection bumped to match. All 30 tests pass. MySql net10 keeps its pre-existing NU1608 (Pomelo has no EF Core 10 release yet). --- ...ic.Core.Data.EntityFramework.MongoDb.Net10.csproj | 8 ++++---- ...tic.Core.Data.EntityFramework.MongoDb.Net8.csproj | 8 ++++---- ...ntic.Core.Data.EntityFramework.MySql.Net10.csproj | 6 +++--- ...antic.Core.Data.EntityFramework.MySql.Net8.csproj | 6 +++--- ...Core.Data.EntityFramework.PostgreSql.Net10.csproj | 6 +++--- ....Core.Data.EntityFramework.PostgreSql.Net8.csproj | 6 +++--- ...antic.Core.Data.EntityFramework.Relational.csproj | 4 ++-- ....Core.Data.EntityFramework.SqlServer.Net10.csproj | 10 +++++----- ...c.Core.Data.EntityFramework.SqlServer.Net8.csproj | 10 +++++----- .../eQuantic.Core.Data.EntityFramework.Net10.csproj | 6 +++--- .../eQuantic.Core.Data.EntityFramework.Net8.csproj | 6 +++--- .../eQuantic.Core.Data.EntityFramework.csproj | 12 ++++++------ ....Core.Data.EntityFramework.SqlServer.Tests.csproj | 4 ++-- .../eQuantic.Core.Data.EntityFramework.Tests.csproj | 4 ++-- 14 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj index dca97c2..2a64b88 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj @@ -22,16 +22,16 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj index c642da5..76c7c37 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj @@ -22,16 +22,16 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj index fd1c94b..6de303c 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj @@ -19,15 +19,15 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj index 9e3e06b..9af0202 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj @@ -19,15 +19,15 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj index d1680a7..ded86c3 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj @@ -19,15 +19,15 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj index 244d5db..faef4c3 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj @@ -19,15 +19,15 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj index d4e9992..772d6c0 100644 --- a/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj @@ -21,11 +21,11 @@ - + - + diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj index d1cb529..44fd947 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj @@ -19,15 +19,15 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + Version="10.0.10" /> + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj index 693acad..a4c2c46 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj @@ -19,15 +19,15 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + Version="8.0.29" /> + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj index 7de55e9..ab20736 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj @@ -19,12 +19,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj index 2fd1bcd..ac1500b 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj @@ -19,12 +19,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj index 04b3636..776afc9 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj @@ -21,24 +21,24 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj index 1dfbae9..e240577 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj index e777726..ebfc570 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj @@ -10,8 +10,8 @@ - - + + From 74119e0312785ce001fdab739dc23b71fa0b0675 Mon Sep 17 00:00:00 2001 From: Edgar Mesquita Date: Mon, 20 Jul 2026 17:41:49 +0100 Subject: [PATCH 32/32] =?UTF-8?q?=F0=9F=91=B7=20ci:=20define=20token=20per?= =?UTF-8?q?missions=20at=20the=20job=20level?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SonarCloud flags workflow-level permissions ('Read permissions should be defined at the job level'). Move 'permissions: contents: read' from the workflow level to each job (build/test in ci.yml; build/test/publish in release.yml) — this satisfies both the CodeQL 'workflow does not contain permissions' rule and the SonarCloud job-level rule, restoring the Security Rating on new code. --- .github/workflows/ci.yml | 7 ++++--- .github/workflows/release.yml | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f19a70f..5f778c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,12 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - contents: read - jobs: build: name: Build ${{ matrix.project }} runs-on: ubuntu-latest + permissions: + contents: read strategy: fail-fast: false matrix: @@ -49,6 +48,8 @@ jobs: test: name: Test ${{ matrix.project }} runs-on: ubuntu-latest + permissions: + contents: read strategy: fail-fast: false matrix: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53dc059..cc821e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,13 +12,12 @@ concurrency: group: release-${{ github.ref }} cancel-in-progress: false -permissions: - contents: read - jobs: build: name: Build ${{ matrix.project }} runs-on: ubuntu-latest + permissions: + contents: read strategy: fail-fast: false matrix: @@ -60,6 +59,8 @@ jobs: test: name: Test ${{ matrix.project }} runs-on: ubuntu-latest + permissions: + contents: read strategy: fail-fast: false matrix: @@ -85,6 +86,8 @@ jobs: publish: name: Publish to NuGet.org runs-on: ubuntu-latest + permissions: + contents: read needs: [build, test] # This environment gates the publish behind whatever protection rules are configured for it in # the repo's Settings -> Environments (e.g. required reviewers). GitHub auto-creates an environment