Skip to content

(MAJOR): Target net10.0 only - #140

Merged
PaulTrampert merged 1 commit into
release/2.0from
feature/76-target-net10
Oct 4, 2026
Merged

PaulTrampert merged 1 commit into
release/2.0from
feature/76-target-net10

Conversation

@PaulTrampert

Copy link
Copy Markdown
Owner

Cause

The repository straddled two target frameworks: the core, Schema, Swashbuckle, Sample and most test projects were net8.0, while PTrampert.SimplePatch.OpenApi (and its tests) were net10.0 because OpenApiSchemaTransformerContext.GetOrCreateSchemaAsync only exists from .NET 10. .NET 8 reaches end of support in November 2026.

Fix

  • Every project, including Test.External and the Sample, now targets net10.0 only.
  • System.CodeDom 9.0.8 → 10.0.12, the framework-aligned version for the new TFM.
  • Dropped the System.Text.Json package reference: it is in the net10.0 shared framework, so the package was redundant.
  • Removed the now-stale csproj comments in Schema and OpenApi that explained the TFM split.
  • The net10.0 analyzers raised one new warning, CA2264 on ArgumentNullException.ThrowIfNull(left) in Optional<T>.operator ==. Optional<T> is a record struct, so boxing it never gives null and the call never threw. I removed it, so behaviour is unchanged.
  • No #if NET8_0-style conditionals existed, so there were none to remove.
  • CI: dotnet-library.yml uses the shared workflow, which reads the SDK from global.json (already 10.0.x). Nothing in .github/workflows/ assumes a TFM.
  • Docs: README has a new Requirements section (.NET 10 or later; .NET 8/9 users stay on 1.x) and drops the "(.NET 10+)" qualifier on the OpenApi heading. docs/getting-started.md loses its ".NET 10 and later" qualifier too. The OpenAPI proposal is a design record, so I left its §3/§4 tables alone and added a note that they describe 1.x. The AGENTS.md Layout table drops its Target column and now says that everything targets net10.0.

Breaking: consumers on .NET 8 or .NET 9 can't take 2.x, including PTrampert.SimplePatch.Swashbuckle, which today gives them full OpenAPI support.

Alternatives rejected

  • Multi-target net8.0;net10.0. This would keep .NET 8/9 consumers on new releases, but it doubles the build and test matrix for a runtime that is weeks from end of support. It also keeps the OpenApi package an asymmetric exception. The issue's goal is one TFM.
  • Keep the System.Text.Json package at 10.x. On net10.0 it is redundant with the shared framework and only adds a dependency to the nuspec.
  • Suppress CA2264. The flagged call was dead code, so removing it is cleaner than suppressing the warning.

Open question for the maintainer

The issue asks whether the net8.0 line gets a maintenance branch (for example release/1.x) for security fixes until .NET 8 reaches end of support. This PR doesn't create one. That's your call.

Test results

dotnet build gave 0 errors and 21 warnings, the same unique warning set as release/2.0 (14 distinct warnings, compared before and after with a clean --no-incremental build).

dotnet test, all on net10.0:

  • PTrampert.SimplePatch.Test: 129 passed, 0 failed
  • PTrampert.SimplePatch.Swashbuckle.Test: 18 passed, 0 failed
  • PTrampert.SimplePatch.OpenApi.Test: 12 passed, 0 failed

Closes #76

🤖 Generated with Claude Code

Move every project from net8.0 to net10.0, so the repository has a
single target framework. .NET 8 reaches end of support in November 2026,
and PTrampert.SimplePatch.OpenApi was already net10.0-only.

- Bump System.CodeDom to 10.0.12 and drop the System.Text.Json package
  reference, which is part of the net10.0 shared framework.
- Remove a no-op ArgumentNullException.ThrowIfNull on the Optional<T>
  struct, which the net10.0 analyzers flag as CA2264.
- Update README (new Requirements section), docs and AGENTS.md.

Closes #76

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PaulTrampert
PaulTrampert force-pushed the feature/76-target-net10 branch from 21c6e26 to 444ef8e Compare October 4, 2026 21:30
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown

✅ PR Title Formatted Correctly

The title of this PR has been updated to match the correct format. Thank you!

@PaulTrampert
PaulTrampert merged commit b4559d0 into release/2.0 Oct 4, 2026
5 checks passed
@PaulTrampert
PaulTrampert deleted the feature/76-target-net10 branch October 4, 2026 21:40
PaulTrampert added a commit that referenced this pull request Oct 4, 2026
…make PatchClassBuilder static (#141)

## Cause

#133 made `PatchClassBuilder` a facade over the internal
`RoslynPatchClassBuilder`, and #95 added
`UseExperimentalDynamicClassBuilder`, which the facade's
`GetPatchClassFor` read on every call. Both kept
`PatchClassBuilder.Instance` typed as `PatchClassBuilder`, because
changing a property's type breaks binary compatibility. This is the 2.0
change that #135 deferred.

Once `Instance` hands out the selected internal builder, nothing hands
out a `PatchClassBuilder` any more. Its instance surface (the obsolete
constructor, instance `GetPatchClassFor`, and its `IPatchClassBuilder`
implementation) only forwards to `Instance` and is dead weight (#143).
The two changes are one PR because a static class can't be a property
type, so #143 can't compile without #135.

## Fix

**Retype `Instance` (#135)**
- `public static IPatchClassBuilder Instance` returns the selected
builder itself: `EmitPatchClassBuilder.Instance` when the flag is on,
otherwise `RoslynPatchClassBuilder.Instance`. Both internal builders
already had static `Instance` singletons.
- `Instance` is a computed property (`=>`), not an initialised one, so
it honours the flag at read time. The flag is a settable static, and the
tests flip it back and forth.
- Call sites (`PatchJsonConverterFactory`, the Swashbuckle filter, the
OpenAPI transformer) already call
`PatchClassBuilder.Instance.GetPatchClassFor(...)` on each use, so they
compile and behave unchanged against the new type. None of them caches
the builder.

**Make `PatchClassBuilder` static (#143)**
- `public static class PatchClassBuilder` now holds only `Instance` and
`UseExperimentalDynamicClassBuilder`.
- The constructor, instance `GetPatchClassFor` and the
`IPatchClassBuilder` implementation are removed.
- The XML docs worth keeping moved. The description of the generated
class (its properties, its `Patch` method, sealed and public, the
`.Optionals` namespace) is now in the class remarks. The
`NotSupportedException` conditions are now in `Instance`'s remarks.
`IPatchClassBuilder.GetPatchClassFor` keeps its builder-neutral
contract. `RoslynPatchClassBuilder.GetPatchClassFor` had `<inheritdoc
cref="PatchClassBuilder.GetPatchClassFor"/>` and now has its own summary
and exception docs.
-
`RoslynPatchClassBuilderTest.GetPatchClassFor_SharesGeneratedTypesAcrossBuilders`
constructed `PatchClassBuilder` twice. It is now
`GetPatchClassFor_CachesTheGeneratedType`, which calls
`RoslynPatchClassBuilder.Instance` twice and still checks that
`PatchClassBuilder.Instance` agrees with it while the flag is off. The
internal builders have private constructors, so a second instance can't
be constructed.

**Docs:** `README.md` and `docs/getting-started.md` note that `Instance`
returns the selected builder, so you should read it where you use it and
not keep it. `AGENTS.md` describes the static class.

**Breaking:**
- Binaries compiled against `PatchClassBuilder
PatchClassBuilder.Instance { get; }` fail with `MissingMethodException`.
- `new PatchClassBuilder()` (obsolete since 1.x) and instance
`GetPatchClassFor` are gone.
- `PatchClassBuilder` no longer implements `IPatchClassBuilder`, and
can't be used as a variable, parameter or generic argument type.

## Coordination with sibling PRs into `release/2.0`

- **#139 (#75) makes the constructor internal.** This PR deletes the
constructor, which supersedes that change. When the two meet, resolve
the conflict by deleting the constructor.
- **#142 (#126)** makes Emit the only runtime builder and removes the
flag. Once it lands, `Instance` collapses to
`EmitPatchClassBuilder.Instance`.
- **#140 (#76)** changes TFMs. They are unchanged here.

## Alternatives rejected

- **Initialise `Instance` once (`{ get; } = ...`).** This would freeze
whatever builder the flag selected at type initialisation and ignore
later changes to the flag.
- **Keep returning the facade, typed as the interface.** This would
avoid the read-time caveat, but it keeps the per-call forwarding that
#135 asks to remove. It also leaves `Instance` as the one thing that
hands out a `PatchClassBuilder`.
- **Keep `PatchClassBuilder` non-static, with the constructor internal
(#75 alone).** Nothing would construct it, so its instance members would
be unreachable dead code.
- **Ship #143 as a separate PR stacked on this one.** The maintainer
chose to fold it in, because #143 can't compile without the retype.

## Tests

New tests in `UseExperimentalDynamicClassBuilderTest`:
- `Instance_IsTheRoslynBuilderWhenOff`
- `Instance_IsTheEmitBuilderWhenOn`
- `Instance_FollowsTheFlagWhenItIsTurnedBackOff`

Against the old `PatchClassBuilder.cs`, the NUnit analyzer rejects all
three with NUnit2020 (a `SameAs` that always fails because the types are
mutually exclusive), so they fail before the fix.

CI's build pipeline doesn't run on PRs that target a branch other than
`main`, so these results are local only:
- `dotnet build`: 0 errors, 21 warnings, the same count as the base
commit.
- `dotnet test`: all passed. Core 132/132, Swashbuckle 18/18, OpenApi
12/12. The net8.0 test hosts ran with `DOTNET_ROLL_FORWARD=Major`,
because only the .NET 10 runtime is installed locally.

Closes #135
Closes #143

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@PaulTrampert PaulTrampert mentioned this pull request Oct 4, 2026
PaulTrampert added a commit that referenced this pull request Oct 4, 2026
Ships the 2.0 major release from `release/2.0`.

> [!IMPORTANT]
> Merge with **Rebase and merge**, never squash. Release notes are built
from the commit subjects since `v1.5.0`, so a rebase merge gives each PR
below its own line. A squash would collapse them into one.

## Included

- #139 Make `PatchClassBuilder`'s constructor internal (closes #75)
- #140 Target net10.0 only (closes #76)
- #141 Retype `PatchClassBuilder.Instance` to `IPatchClassBuilder` and
make `PatchClassBuilder` static (closes #135, closes #143)
- #142 Make Reflection.Emit the only runtime patch class builder (closes
#126)

## Breaking changes

- **.NET 10 or later only.** .NET 8 and 9 consumers stay on 1.x.
- **`PatchClassBuilder` is a static class.**
`PatchClassBuilder.Instance` is typed `IPatchClassBuilder`, and the
constructor is gone.
- **`UseExperimentalDynamicClassBuilder` is removed.** Reflection.Emit
is the only runtime builder. Internal write models are supported when
their assembly declares
`[InternalsVisibleTo("PTrampert.SimplePatch.Emitted")]`. The Roslyn
builder is kept but unused at runtime, pending #144.

## Branch state

`release/2.0` has no commits behind `main` and no merge commits. All
four commits are `(MAJOR)`, so the version calculation produces 2.0.0.

Postponed until after 2.0: #96 and #102, with open questions noted on
each.

## Tests

Each included PR passed the CI build and tests against `release/2.0`.
This PR's build runs the full suite on the combined branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant