Date: 2026-10-03
Accepted again by ADR-0014, which withdraws ADR-0012's two release trains: every package is released at one version. ADR-0014 also keeps Dependabot's ignore rules, which ADR-0012 widened, and the README each package carries. The body below is kept as the record of the decision as it was taken.
The twelve packages are released together: semantic-release computes one version from the Conventional Commits,
release-pack.sh packs every packable project at it, and the GitHub Release links all twelve. Several of them sit on
a framework that ships a major every November. AdCodicem.ValueObjects.EntityFrameworkCore and
AdCodicem.ValueObjects.Identifiers.EntityFrameworkCore depend on Entity Framework Core,
AdCodicem.ValueObjects.AspNetCore on ASP.NET Core, AdCodicem.ValueObjects.OpenApi on
Microsoft.AspNetCore.OpenApi and on Microsoft.OpenApi, which has majors of its own, and the generator is compiled
against Roslyn. .NET 11 is at its first release candidate, published on 2026-09-08; its release is expected around
2026-11-10, a date not confirmed.
What a package asks of its dependencies is a floor. CentralPackageTransitivePinningEnabled makes every package
Directory.Packages.props pins a direct dependency of the package that pulls it, so each nuspec names the central
version as its minimum, with no upper bound: Microsoft.EntityFrameworkCore 10.0.12 or later, for instance. Every
bump of a shipped dependency raises a floor.
Libraries in the same position answer the next framework major in two ways:
- Follow its major. Npgsql's EF Core provider, Pomelo's and
EFCore.NamingConventionsnumber their releases after EF Core's and bound it:Npgsql.EntityFrameworkCore.PostgreSQL10.0.3 asks for EF Core[10.0.4, 11.0.0),EFCore.NamingConventions10.0.1 for[10.0.1, 11.0.0). Their users wait for a new major of theirs at each EF Core major. They are providers, built on EF Core's internals. - Keep a version of their own. Vogen and StronglyTypedId do. So does Thinktecture.Runtime.Extensions, but it
ships one EF Core package per EF Core major:
Thinktecture.Runtime.Extensions.EntityFrameworkCore8,…9and…10, all at 10.5.0.
What the packages built for .NET 10 do on .NET 11 was measured rather than assumed. A test project outside the
solution, tests/Compat, installs the twelve packages as a consumer does into a net11.0 application on SDK
11.0.100-rc.1: the generator ran in that SDK's compiler (Roslyn 5.11), and the 53 tests that need no container
passed, over Entity Framework Core 11 on SQLite, System.Text.Json source generation, ASP.NET Core model binding,
Microsoft.AspNetCore.OpenApi 11, FluentValidation, Dapper, Newtonsoft.Json and the contract kit. Its PostgreSQL tests
passed against a local server; its SQL Server tests run only in CI. Yet the frameworks do break their binary
interface between majors: ApiCompat reports 230 breaking differences between Microsoft.EntityFrameworkCore 10.0.12
and 11.0.0-rc.1, 97 for Microsoft.EntityFrameworkCore.Relational, and 31 between Microsoft.OpenApi 2.12.2 and
3.10.0, the version Microsoft.AspNetCore.OpenApi 11 brings. None of them is on a path the integrations take: the
schema transformer, compiled against Microsoft.OpenApi 2, wrote every keyword it writes on 3.10.0. System.Text.Json
reports no breaking difference from one major to the next, 6.0 to 10.0.
We will release the twelve packages under one SemVer version, driven by our own API alone and never aligned with the major of .NET or of EF Core, and support a framework's next major in the same packages, never through a package per framework major.
- One version for the twelve. A consumer references the same version of each.
- Support is stated, not encoded in package IDs. Each package's target framework,
net10.0today, says where it installs: on .NET 10 or any later version. Its dependencies are minimums with no upper bound, as Microsoft's library guidance recommends: an upper bound fails a restore that pairs the package with anything newer, breaking or not. The support table names, for each package, the framework majors it is built and tested against and what the compatibility job checks on the next one. It names majors; the exact minimum of each dependency is the package's dependency list on nuget.org. - A ladder, climbed only as far as the code requires.
- Rung 0, where every package is today: one target framework, floors on the current majors. An application on a newer framework takes the newer major itself, and the packages run on it.
- Rung 1: the same package targets a second framework, only when its code has to differ for a framework major
and that major has a target framework of its own.
AdCodicem.ValueObjects.OpenApiis the only candidate:Microsoft.AspNetCore.OpenApi11 is fornet11.0only and bringsMicrosoft.OpenApi3, so a transformer that needed different code for it would carry it in anet11.0target. - No target added ahead of need for the EF Core packages. A
net11.0dependency group would ask everynet11.0project for EF Core 11, and one that stays on EF Core 10 with Npgsql's provider 10, which caps EF Core below 11, would fail to restore with NU1107.
- When a major cannot be told apart by target framework, a new major of a dependency on the same target framework, the floor stays on the old major for as long as the compatibility job passes on the new one. When a binary break means one assembly can no longer serve both, the floor moves to the new major. That is a breaking change of ours, released as one, and users of the old major stay on the version before it: there is no maintenance line.
- The compatibility island.
tests/Compatholds the next .NET major's application: its ownglobal.json, naming the release candidate's SDK, which CI installs exactly (setup-dotnetignoresrollForwardfor a prerelease version) and whichrollForward: latestFeaturelets a later 11.0 SDK stand in for locally, its ownDirectory.Build.propsandDirectory.Packages.props, with no transitive pinning, and anuget.configthat takes the twelve packages fromartifacts/packagesalone, at exactly the version just packed. It is in no solution. Thecompat (.NET 11)job ofci.ymlruns it on every pull request and push tomain, against the packages the build job packed, with PostgreSQL 17 and SQL Server in containers. It informs and blocks nothing until .NET 11 ships, then becomes a required check. - A breaking change bumps the minor while the major is 0. The first rule of the commit analyzer's
releaseRulesin.releaserc.jsonis{ "breaking": true, "release": "minor" }: afeat!, afix(deps)!or aBREAKING CHANGE:footer releases0.y+1.0. At 1.0 the rule is replaced by{ "breaking": true, "release": "major" }, not deleted: without a breaking rule of its own,build(pack)!anddocs(readme)!would fall back to the patch their scope is rated. Before 1.0.0, a minor may therefore break, deprecate or remove part of the public API, deprecated members included: the deprecatedPattern,MinimumandMaximumoptions say that any minor version may remove them before 1.0.0. (Amended: this bullet first promised that a deprecated option would stay until 1.0.0.) Only from 1.0.0 on does a breaking change wait for a major. - Dependabot follows the same lines.
- A new major of a framework is supported by a decision, a floor or a target framework, never by a bump: the
nugetentry ignores semver-major updates ofMicrosoft.EntityFrameworkCore*,Microsoft.AspNetCore.*,Microsoft.Extensions.*,Microsoft.OpenApiandNpgsql.EntityFrameworkCore.PostgreSQL. Dependabot's NuGet updater applies those rules to security updates too, so a security fix that exists only on a new major of one of them opens no pull request: its alert stays open, and it is handled as a deliberate migration under this record. A fix published on the current major opens at once. - The
nuget,npmandgithub-actionsentries propose a version once it has been public for seven days (cooldown); security updates are not delayed. - A bump of a dependency the packages ship has to release a patch. Dependabot takes one
nugetentry per directory, so that entry keeps thechore(deps)prefix, anddependabot-auto-merge.ymlretitles the pull requestfix(deps): …when one of the dependencies it updates is listed in.github/shipped-dependencies, andfix(deps)!: …when that update is a semver-major. The list holds the eleven dependencies the nuspecs name, plusMicrosoft.CodeAnalysis.CSharpandPolySharp, compiled into the generator the main package carries; not MinVer, SourceLink orMicrosoft.CodeAnalysis.Analyzers, which shape the build but not what ships. It names exact IDs, and a step ofci.yml, run on the packed nuspecs, fails when the two drift apart. Every other NuGet update keepschore(deps), and the other ecosystems keep the prefixdependabot.ymlgives them,ci(deps)orchore(deps); none of them releases anything. A Dependabot pull request is squash-merged, so that its title is the commit semantic-release reads: auto-merge does so already, and a pull request merged by hand must be squashed too.
- A new major of a framework is supported by a decision, a floor or a target framework, never by a bump: the
- A package per framework major, as Thinktecture does for EF Core. The API would be the same in each, a consumer would have to pick the right one, and every major would add a package ID to publish for good.
- A major aligned with the framework's, as the EF Core providers do. The version would stop saying anything about our own API: a breaking change of ours would wait for November or break the alignment. A provider is built on EF Core's internals; these integrations call its public surface.
- Targeting the next framework ahead of need, for the NU1107 above.
- Upper bounds on dependencies. They would stop an application from taking the next major even where nothing breaks, which is most of the time.
- Maintenance branches for an older framework line. Stable releases are cut from
mainonly, and one maintainer cannot carry two lines.
A floor rises with every bump of a shipped dependency, so the support table names majors and leaves the exact minimum to nuget.org, where it cannot drift.
An application held on an older framework major gets no fixes once the floor moves: it stays on the last version
before the move. SECURITY.md says the same: only the latest stable release receives fixes.
The island is maintained by hand. Dependabot finds projects through AdCodicem.ValueObjects.slnx and never sees it,
and its NuGet image has no .NET 11 SDK. Each release candidate is a pull request that moves the island's
global.json and its .NET 11 packages together, Npgsql's provider with EF Core, since it pins EF Core exactly; the
release of .NET 11 moves them to 11.0.0 and makes the job required (docs/maintaining.md). When .NET 12 previews
arrive, moving the island to the next major, or adding a second one, is a decision of its own, and renaming the job
changes the required check.
Tools that walk the repository rather than the solution do see the island. CodeQL's default setup downloads every SDK
a global.json names and analyses its code. GitHub's automatic dependency submission restores every project file
from the root with the .NET 10 SDK, so the island's project file leaves itself empty on an SDK that cannot target
net11.0, and its restore succeeds with nothing to submit.
Microsoft.CodeAnalysis.CSharp is on the shipped list, so a minor of Roslyn auto-merges as fix(deps) and raises the
compiler the generator needs, and a major is titled fix(deps)! and left for a human. A grouped Dependabot pull
request that mixes shipped and test-only dependencies becomes one fix(deps). One merged with a rebase keeps
Dependabot's chore(deps) commit and releases nothing.