✨ Migrate to eQuantic.Core.Data v5 contracts + per-major (net8/net10) packaging - #1
Merged
Merged
Conversation
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
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
…orrected) 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
…tion 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
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcFK9ZPmuzQKRamwtvFe3s
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).
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.
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.
| 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.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



v5 — migration to the
eQuantic.Core.Data5.1.0 contractsThis 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
QueryOptions<TEntity>(filter / specification / includes /sortings / tracking / query-filter / tag / before-&-after customization) instead of
Action<QueryableConfiguration>+ a wall of overloads; paged reads returnPagedResult<T>and countsreturn
long.IRepository<TEntity, TKey>— entities are constrained toIEntity<TKey>(nonew()), and the write surface gainsAddRangeAsyncplus aCancellationTokenonAddAsync.QueryOptions → IQueryabletranslator applies the options in a defined order and stays server-side /EF-translatable (sortings included).
(~1,160 lines) to ~230 while keeping full sync + async read/write parity; the injected
UnitOfWorkisstill 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.Configabstractions. They are rehomed into theRelationalpackage undereQuantic.Core.Data.EntityFramework.Relational.Sql(
ISqlExecutor,IAsyncSqlExecutor,ISqlUnitOfWork,ParamValue,SqlConfiguration); the deadQueryableConfiguration/ISqlRepositorywere dropped. The SQL-injection fix (S1) is preserved verbatim— values still travel as
DbParameters and are never interpolated into the command text orFromSqlRawtemplate, and the parameterization regression test moved with them and still passes.
Packaging & versioning (resolves the "parallel version lines" question)
net8.0andnet10.0only — net6/net7/net9 removed.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-target base is 4.5.0 and the new
Relationalpackage is 4.0.0.eQuantic.Core.Data5.1.0.CI / release
ci.ymlandrelease.ymlbuild matrices are pruned to the 12 surviving csprojs (and pick up therenamed
MongoDb.Net8).adopted here: per-major (8.x / 10.x) versioning is incompatible with its single-version model.
Verification
Earlier phases (0–2)
Context
Deep analysis of this repository (published on nuget.org for years) and of the
eQuantic/core-datacontracts 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 replacedwith real coverage (including a new MongoDb test project).
Phase 1 — Code fixes
SqlExecutorinterpolated values into the SQL text and passed the result toFromSqlRawwith noDbParameter— a real injection inExecuteFunction/ExecuteProcedureacross the 3 SQL providers. Now emits placeholders and the values travel asDbParameters. Tests prove a malicious value never reaches the generated SQL.EXEC→CALL(P3)EXEC(T-SQL) copied from SqlServer — fails at runtime. Now useCALL.ExecuteQuerypassed anIEnumerableas a single argument toFromSqlRaw— fixed with.ToArray().ISqlUnitOfWorkwas registered unconditionally →InvalidCastExceptionfor MongoDb. Now registered only when the implementation provides it.AsyncQueryableRepositoryshadowed_disposedand disposed theUnitOfWorktwice.GetDatabase(_collectionName)used the collection name as the database name → deletes/updates silently against the wrong database. Now resolves the database from theDbContext'sMongoOptionsExtensionand throws instead of returning0.UpdateDefinitionBuilderevaluatedx => x.Count + 1against a default instance and wrote the constant1. Now rejects (throws on) update expressions that reference the entity.All/Anyignored config (A1)configuration; they now forward it.Get(0)/Guid.EmptythrewArgumentNullException. Now validatesid is null.GetFindByKeyExpressionembedded the key value as a literal → query-cache pollution. Now parameterizes via a closure, caches PK metadata, and usesEF.Property(shadow keys).GetPaged/GetPagedAsyncdidSkip/Takewithout a guaranteedOrderBy. Now order by the primary key when there is no explicit ordering.AddRepositoryhonours the configured lifetime, usesTryAddand toleratesReflectionTypeLoadException.ConfigureAwait(false)across 91 library awaits (base + 4 providers).The repository no longer disposes the injected
UnitOfWork— the creator (DI container or caller) ownsthe lifetime. Before, under DI, this disposed the shared
DbContextout from under the other repositoriesin the scope. Anyone who relied on disposing the repository to close a manually-created context must now
dispose the
UnitOfWork/DbContextdirectly.Note on M8: the plan finding (remove
Where(_ => true)inGetAllAsync) was wrong — thatWhereis load-bearing. Documented in code + a regression test; nothing removed.
Phase 0 — CI/release
on: [push]published to nuget.org on every push to any branch. CI split intoci.yml(build + test, never publishes) andrelease.yml(only on avX.Y.Ztag, publishes behind anuget-releaseGitHub Environment).ci.ymlnow runsdotnet teston the 3 test projects.ubuntu-latest, NuGet cache,-p:ContinuousIntegrationBuild=true.global.jsonpinning the SDK.build/MSBump.props(circular import),build/MSBump.targets(missing task) andbuild/Directory.Build.targets(never imported).MySql.Net10.csprojreferenced 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.SqlExecutor,UnitOfWork,SetandExpressionConverterwerenear-identical copies. They now live once in the shared package. Each provider keeps thin
SetandUnitOfWork<TDbContext>subclasses plusDefaultUnitOfWork, so the consumer-facing types stay in theirnamespaces. ~2,200 fewer lines of duplicated source (3,029 removed / 799 added).
EXECon SQL Server, ANSICALLelsewhere) is asingle
BuildProcedureSqlvirtual, overridden only by SQL Server — the copy-paste that letEXEC/CALLdiverge is gone.
Microsoft.EntityFrameworkCore.Relationalinto MongoDb consumers, and linked source would collide when aconsumer references two providers. A separate multi-target assembly is the only clean option; see the plan
for the full reasoning.
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-genericUnitOfWorkand
SqlConfigurationExtensionsmove to theRelationalnamespace, andGetEntityByIdSpecificationnowtakes
RelationalUnitOfWork. The consumer-facingDefaultUnitOfWork/UnitOfWork<TDbContext>/Set<TEntity>are unchanged.git tag vX.Y.Z && git push origin vX.Y.Z.nuget-releaseEnvironment has no protection until configured in Settings → Environments(this session cannot configure it). Without a required reviewer there, the gate is only nominal.
Relationalpackage (version1.0.0) is a new published package the SQL providers nowdepend on; its versioning must join whichever scheme is chosen in Part IV.
Out of scope (larger phases, awaiting a chosen approach)
eQuantic.Core.Data) and consolidation of the parallel versionlines on nuget.org (the decision that also blocks MinVer adoption).
$inc/pipeline instead of rejecting them.