Skip to content

✨ Migrate to eQuantic.Core.Data v5 contracts + per-major (net8/net10) packaging - #1

Merged
edgarmesquita merged 32 commits into
masterfrom
claude/repo-improvement-analysis-7emny7
Jul 20, 2026
Merged

edgarmesquita merged 32 commits into
masterfrom
claude/repo-improvement-analysis-7emny7

Conversation

@edgarmesquita

@edgarmesquita edgarmesquita commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

v5 — migration to the eQuantic.Core.Data 5.1.0 contracts

This PR now also delivers what it originally listed as out-of-scope (Phases 3/4): the provider is
migrated to the eQuantic.Core.Data v5 contracts, and the parallel nuget.org version lines are
consolidated into an explicit scheme (see Packaging & versioning below). The Phase 0–2 record is kept
underneath for reference.

Contracts

  • Reads are shaped through a single QueryOptions<TEntity> (filter / specification / includes /
    sortings / tracking / query-filter / tag / before-&-after customization) instead of
    Action<QueryableConfiguration> + a wall of overloads; paged reads return PagedResult<T> and counts
    return long.
  • The unit-of-work type parameter is gone — IRepository<TEntity, TKey> — entities are constrained to
    IEntity<TKey> (no new()), and the write surface gains AddRangeAsync plus a
    CancellationToken on AddAsync.
  • A QueryOptions → IQueryable translator applies the options in a defined order and stays server-side /
    EF-translatable (sortings included).
  • The composed repositories now inherit read and delegate write, collapsing the old overload-delegation
    (~1,160 lines) to ~230 while keeping full sync + async read/write parity; the injected UnitOfWork is
    still never disposed by the repository (the C2 ownership fix is preserved).

SQL/Config rehomed to the provider layer

core-data v5 removed the Repository.Sql / Repository.Config abstractions. They are rehomed into the
Relational package
under eQuantic.Core.Data.EntityFramework.Relational.Sql
(ISqlExecutor, IAsyncSqlExecutor, ISqlUnitOfWork, ParamValue, SqlConfiguration); the dead
QueryableConfiguration/ISqlRepository were dropped. The SQL-injection fix (S1) is preserved verbatim
— values still travel as DbParameters and are never interpolated into the command text or FromSqlRaw
template, and the parameterization regression test moved with them and still passes.

Packaging & versioning (resolves the "parallel version lines" question)

  • Targets trimmed to net8.0 and net10.0 only — net6/net7/net9 removed.
  • Per-major publish packages (what a consumer picks by runtime): base +
    SqlServer/PostgreSql/MySql/MongoDb at 8.2.0 (net8 / EF Core 8) and 10.1.0 (net10 / EF Core 10) —
    same PackageId across the 8 and 10 lines.
  • Multi-framework lane kept in major 4 so it is never confused with a .NET version: the referenceable
    multi-target base is 4.5.0 and the new Relational package is 4.0.0.
  • All packages reference eQuantic.Core.Data 5.1.0.

CI / release

  • Both ci.yml and release.yml build matrices are pruned to the 12 surviving csprojs (and pick up the
    renamed MongoDb.Net8).
  • The tag-triggered, environment-gated release flow is retained. Semantic-release is intentionally not
    adopted here: per-major (8.x / 10.x) versioning is incompatible with its single-version model.

Verification

  • All 12 csprojs build clean on net8 and net10.
  • 29 tests pass — base 8, SqlServer 18, MongoDb 3 — all on EF Core InMemory (no database required).

Earlier phases (0–2)

Context

Deep analysis of this repository (published on nuget.org for years) and of the eQuantic/core-data
contracts repository, followed by the implementation of Phase 1 (code fixes), Phase 0 (CI/release
pipeline) and Phase 2 (provider de-duplication). The full diagnosis, the 5-phase plan and the
contract-break analysis for a future v5.0.0 are in docs/IMPROVEMENT_PLAN.md.

Verification: all 6 packages build multi-target (base/Relational/SqlServer/PostgreSql/MySql on
net6–net10, MongoDb on net8–net10) and 28 tests pass. The placebo test (Assert.Pass()) was replaced
with real coverage (including a new MongoDb test project).

Phase 1 — Code fixes

Sev. Finding Fix
🔴 SQL injection (S1) SqlExecutor interpolated values into the SQL text and passed the result to FromSqlRaw with no DbParameter — a real injection in ExecuteFunction/ExecuteProcedure across the 3 SQL providers. Now emits placeholders and the values travel as DbParameters. Tests prove a malicious value never reaches the generated SQL.
🟠 EXEC→CALL (P3) PostgreSQL/MySQL used EXEC (T-SQL) copied from SqlServer — fails at runtime. Now use CALL.
🟠 Lost parameters (P4) ExecuteQuery passed an IEnumerable as a single argument to FromSqlRaw — fixed with .ToArray().
🔴 DI crash (C3) ISqlUnitOfWork was registered unconditionally → InvalidCastException for MongoDb. Now registered only when the implementation provides it.
🔴 Double-dispose (C1) AsyncQueryableRepository shadowed _disposed and disposed the UnitOfWork twice.
🔴 MongoDb wrong database (P1/P6) GetDatabase(_collectionName) used the collection name as the database name → deletes/updates silently against the wrong database. Now resolves the database from the DbContext's MongoOptionsExtension and throws instead of returning 0.
🔴 MongoDb silent corruption (P2) UpdateDefinitionBuilder evaluated x => x.Count + 1 against a default instance and wrote the constant 1. Now rejects (throws on) update expressions that reference the entity.
🟠 All/Any ignored config (A1) The sync variants discarded configuration; they now forward it.
🟠 Default key rejected (A2) Get(0)/Guid.Empty threw ArgumentNullException. Now validates id is null.
🟠 Non-parameterized key (A4/M7) GetFindByKeyExpression embedded the key value as a literal → query-cache pollution. Now parameterizes via a closure, caches PK metadata, and uses EF.Property (shadow keys).
🟠 Non-deterministic pagination (A5) GetPaged/GetPagedAsync did Skip/Take without a guaranteed OrderBy. Now order by the primary key when there is no explicit ordering.
🟡 DI (M4) AddRepository honours the configured lifetime, uses TryAdd and tolerates ReflectionTypeLoadException.
🟡 Async (M2) ConfigureAwait(false) across 91 library awaits (base + 4 providers).

⚠️ Only behavioural change: C2 (UnitOfWork ownership)

The repository no longer disposes the injected UnitOfWork — the creator (DI container or caller) owns
the lifetime. Before, under DI, this disposed the shared DbContext out from under the other repositories
in the scope. Anyone who relied on disposing the repository to close a manually-created context must now
dispose the UnitOfWork/DbContext directly.

Note on M8: the plan finding (remove Where(_ => true) in GetAllAsync) was wrong — that Where
is load-bearing. Documented in code + a regression test; nothing removed.

Phase 0 — CI/release

Finding Fix
Q1 accidental publishing on: [push] published to nuget.org on every push to any branch. CI split into ci.yml (build + test, never publishes) and release.yml (only on a vX.Y.Z tag, publishes behind a nuget-release GitHub Environment).
Q2 no tests in CI ci.yml now runs dotnet test on the 3 test projects.
Q3 dated pipeline Actions updated (v3→v4), ubuntu-latest, NuGet cache, -p:ContinuousIntegrationBuild=true.
Q4 unpinned SDK global.json pinning the SDK.
PK7 dead MSBump Removed build/MSBump.props (circular import), build/MSBump.targets (missing task) and build/Directory.Build.targets (never imported).
PK2 graph bug MySql.Net10.csproj referenced the Net9 core instead of Net10.

Phase 2 — Provider de-duplication

Extracted the shared relational implementation into a new package,
eQuantic.Core.Data.EntityFramework.Relational, referenced by the 3 SQL providers.

  • The SqlServer/PostgreSql/MySql SqlExecutor, UnitOfWork, Set and ExpressionConverter were
    near-identical copies. They now live once in the shared package. Each provider keeps thin Set and
    UnitOfWork<TDbContext> subclasses plus DefaultUnitOfWork, so the consumer-facing types stay in their
    namespaces. ~2,200 fewer lines of duplicated source (3,029 removed / 799 added).
  • The only genuine dialect difference (stored procedures: EXEC on SQL Server, ANSI CALL elsewhere) is a
    single BuildProcedureSql virtual, overridden only by SQL Server — the copy-paste that let EXEC/CALL
    diverge is gone.
  • Why a new package (not the base package or linked source): the base package must not pull
    Microsoft.EntityFrameworkCore.Relational into MongoDb consumers, and linked source would collide when a
    consumer references two providers. A separate multi-target assembly is the only clean option; see the plan
    for the full reasoning.
  • 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.

Minor source break: the implementation-only public types SqlExecutor, the non-generic UnitOfWork
and SqlConfigurationExtensions move to the Relational namespace, and GetEntityByIdSpecification now
takes RelationalUnitOfWork. The consumer-facing DefaultUnitOfWork / UnitOfWork<TDbContext> /
Set<TEntity> are unchanged.

⚠️ Needs a maintainer with GitHub access

  1. Nothing publishes automatically until a tag exists — git tag vX.Y.Z && git push origin vX.Y.Z.
  2. The nuget-release Environment has no protection until configured in Settings → Environments
    (this session cannot configure it). Without a required reviewer there, the gate is only nominal.
  3. The new Relational package (version 1.0.0) is a new published package the SQL providers now
    depend on; its versioning must join whichever scheme is chosen in Part IV.

Out of scope (larger phases, awaiting a chosen approach)

  • Phases 3/4 — v5.0.0 of the contracts (eQuantic.Core.Data) and consolidation of the parallel version
    lines on nuget.org (the decision that also blocks MinVer adoption).
  • P2 (evolution) — support entity-referencing updates via $inc/pipeline instead of rejecting them.

claude added 17 commits July 16, 2026 12:39
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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<T> 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
@edgarmesquita edgarmesquita changed the title Análise de melhoria + correções da Fase 1 (segurança, correção, async) Análise de melhoria + Fases 0 e 1 (segurança, correção, CI/release) Jul 18, 2026
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
claude added 2 commits July 19, 2026 15:44
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<TDbContext> 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).
@edgarmesquita edgarmesquita changed the title Análise de melhoria + Fases 0 e 1 (segurança, correção, CI/release) Improvement plan + Phases 0, 1 and 2 (security, correctness, CI/release, de-duplication) Jul 19, 2026
claude and others added 7 commits July 19, 2026 16:20
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.
… (4.x) lines

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.
- 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<int> (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<int>,
  two-arg repos, Action<config> -> QueryOptions<Product>, 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).
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.
@edgarmesquita edgarmesquita changed the title Improvement plan + Phases 0, 1 and 2 (security, correctness, CI/release, de-duplication) ✨ Migrate to eQuantic.Core.Data v5 contracts + per-major (net8/net10) packaging Jul 20, 2026
Replace the v4 walkthrough (IEntity without key, PagedList<T>, Get(id, lambda),
GetPaged(spec, pageIndex, pageSize, ...)) with a faithful v5 end-to-end slice:
IEntity<TKey> with GetKey/SetKey, UnitOfWork<TDbContext> + AddRelationalRepositories,
GetAsyncQueryableRepository, a single QueryOptions<TEntity> (typed Where/And/Or,
OrderBy, Include, NoTracking), PagedResult<T>, specifications, set-based
UpdateMany/DeleteMany, custom repositories, and a domain service. Every snippet
verified against the v5 contracts and the provider source.
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.
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.
Comment thread .github/workflows/release.yml Outdated
cancel-in-progress: false

permissions:
contents: read
…orkCore (8.4.2/10.0.2)

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).
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.
@sonarqubecloud

Copy link
Copy Markdown

@edgarmesquita
edgarmesquita merged commit 2720983 into master Jul 20, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants