Skip to content

(PATCH): Extract the patch class model from PatchClassBuilder - #129

Merged
PaulTrampert merged 2 commits into
mainfrom
feature/127-extract-patch-class-model
Oct 1, 2026
Merged

PaulTrampert merged 2 commits into
mainfrom
feature/127-extract-patch-class-model

Conversation

@PaulTrampert

Copy link
Copy Markdown
Owner

Closes #127

What and why

Groundwork for the Reflection.Emit builder (#95). The decisions about what a patch class contains move out of PatchClassBuilder.CreatePatchClass into a new internal PatchClassModel, so a second builder can share them instead of copying them and drifting (the bug class behind #87–#93). Doing the move here keeps the Emit PR's diff to new IL only.

PatchClassModel.For(type) holds:

  • IsRecord (the <Clone>$ check),
  • Constructor and ConstructorProperties (System.Text.Json's constructor selection and case-insensitive parameter binding, in parameter order),
  • OptionalProperties and IgnoredProperties (most-derived properties with a public setter or init accessor, or bound by the constructor, split on [JsonIgnore(Condition = Always)]),
  • for each optional property, an OptionalPropertyModel with HasConverter, JsonPropertyName, and Validators (OptionalValidatorModel(type, index), with the index counted per validator type).

PatchClassBuilder turns the model into CodeDom/C# exactly as before. The helper methods moved verbatim (IsRecord became DetectRecord so it doesn't clash with the property). Some things stayed in the builder on purpose, because they're about generating the class, not about what it contains:

  • the IsVisible check and its NotSupportedException, which is a Roslyn restriction. It still runs before the model is built, so the order of exceptions is unchanged;
  • leaving ignored properties out of the with expression for records;
  • naming and escaping.

No public API changes. The model types are internal, and the core project gains [InternalsVisibleTo("PTrampert.SimplePatch.Test")] (as an InternalsVisibleTo item in the csproj).

Alternatives rejected

  • Have the model also say what goes in the constructor and what goes in the initializer for records. That's a choice about how to build the instance, and an Emit builder may make it differently, so it stays in the builder.
  • Keep the JsonConverterAttribute instance in the model instead of a HasConverter flag. The generated attribute only ever needs the source type and property name, so the flag is all a builder needs.

Tests

  • PatchClassBuilderTest passes unchanged.
  • New PatchClassModelTest (10 tests) covers the model directly: positional records, constructor-bound get-only properties (including a [JsonIgnore] one), a get-only property not in the constructor, [JsonConstructor], ambiguous constructors, hidden properties, JsonIgnore conditions, carried attributes, and repeated validators of one type. This is a pure refactor, so these tests pin the behaviour rather than reproduce a bug. They don't compile before the change because the model didn't exist yet.

Results:

  • dotnet build: 0 errors, 21 warnings, the same as main.
  • dotnet test: SimplePatch.Test 86/86, Swashbuckle.Test 17/17, OpenApi.Test 11/11. The machine has no .NET 8 runtime, so the net8.0 test projects ran with DOTNET_ROLL_FORWARD=Major.

🤖 Generated with Claude Code

PaulTrampert and others added 2 commits October 1, 2026 11:50
Move the decisions about what a patch class contains (record detection,
constructor selection and binding, property selection, and the attributes
carried onto each Optional<T> property) into an internal PatchClassModel,
so a second builder can share them. PatchClassBuilder turns the model into
CodeDom exactly as before. The IsVisible check stays in the Roslyn builder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover the cases the builder tests only reach end to end: positional
records, constructor-bound get-only properties, [JsonConstructor], hidden
properties, ignored properties, and repeated validators of one type. The
core project now exposes its internals to the test project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 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 85e950a into main Oct 1, 2026
8 checks passed
@PaulTrampert
PaulTrampert deleted the feature/127-extract-patch-class-model branch October 1, 2026 16:58
PaulTrampert pushed a commit to PaulTrampert/PacmanManager that referenced this pull request Oct 6, 2026
Updated
[PTrampert.SimplePatch](https://github.com/PaulTrampert/PTrampert.SimplePatch)
from 1.3.6 to 2.0.3.

<details>
<summary>Release notes</summary>

_Sourced from [PTrampert.SimplePatch's
releases](https://github.com/PaulTrampert/PTrampert.SimplePatch/releases)._

## 2.0.3

## Changes

- (PATCH): Drop the Roslyn patch class builder's own cache
([#​154](PaulTrampert/PTrampert.SimplePatch#154))



## 2.0.2

## Changes

- (PATCH): Route PatchClassBuilder.Instance through
CachingPatchClassBuilder
([#​155](PaulTrampert/PTrampert.SimplePatch#155))



## 2.0.1

## Changes

- (PATCH): Add an internal CachingPatchClassBuilder decorator
([#​153](PaulTrampert/PTrampert.SimplePatch#153))
- (PATCH): Document the release PR format for staged release branches
([#​152](PaulTrampert/PTrampert.SimplePatch#152))



## 2.0.0

## Changes

- (MAJOR): Release 2.0
([#​147](PaulTrampert/PTrampert.SimplePatch#147))
- (PATCH): Build PRs into release/** branches and document the release
flow
([#​146](PaulTrampert/PTrampert.SimplePatch#146))



## 1.5.0

## What's Changed
* (MINOR): Add PatchClassBuilder.UseExperimentalDynamicClassBuilder by
@​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#136


**Full Changelog**:
PaulTrampert/PTrampert.SimplePatch@v1.4.0...v1.5.0

## 1.4.0

## What's Changed
* (MINOR): Add IPatchClassBuilder, with PatchClassBuilder delegating to
an internal RoslynPatchClassBuilder by @​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#133


**Full Changelog**:
PaulTrampert/PTrampert.SimplePatch@v1.3.8...v1.4.0

## 1.3.8

## What's Changed
* (PATCH): Add an internal Reflection.Emit patch class builder by
@​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#130


**Full Changelog**:
PaulTrampert/PTrampert.SimplePatch@v1.3.7...v1.3.8

## 1.3.7

## What's Changed
* (PATCH): Bump the csharp-deps group with 6 updates by
@​dependabot[bot] in
PaulTrampert/PTrampert.SimplePatch#81
* (PATCH): Add AGENTS.md, CLAUDE.md symlink and /implement-unblocked by
@​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#125
* (PATCH): Extract the patch class model from PatchClassBuilder by
@​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#129


**Full Changelog**:
PaulTrampert/PTrampert.SimplePatch@v1.3.6...v1.3.7

Commits viewable in [compare
view](PaulTrampert/PTrampert.SimplePatch@v1.3.6...v2.0.3).
</details>

Updated
[PTrampert.SimplePatch.Swashbuckle](https://github.com/PaulTrampert/PTrampert.SimplePatch)
from 1.3.6 to 2.0.3.

<details>
<summary>Release notes</summary>

_Sourced from [PTrampert.SimplePatch.Swashbuckle's
releases](https://github.com/PaulTrampert/PTrampert.SimplePatch/releases)._

## 2.0.3

## Changes

- (PATCH): Drop the Roslyn patch class builder's own cache
([#​154](PaulTrampert/PTrampert.SimplePatch#154))



## 2.0.2

## Changes

- (PATCH): Route PatchClassBuilder.Instance through
CachingPatchClassBuilder
([#​155](PaulTrampert/PTrampert.SimplePatch#155))



## 2.0.1

## Changes

- (PATCH): Add an internal CachingPatchClassBuilder decorator
([#​153](PaulTrampert/PTrampert.SimplePatch#153))
- (PATCH): Document the release PR format for staged release branches
([#​152](PaulTrampert/PTrampert.SimplePatch#152))



## 2.0.0

## Changes

- (MAJOR): Release 2.0
([#​147](PaulTrampert/PTrampert.SimplePatch#147))
- (PATCH): Build PRs into release/** branches and document the release
flow
([#​146](PaulTrampert/PTrampert.SimplePatch#146))



## 1.5.0

## What's Changed
* (MINOR): Add PatchClassBuilder.UseExperimentalDynamicClassBuilder by
@​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#136


**Full Changelog**:
PaulTrampert/PTrampert.SimplePatch@v1.4.0...v1.5.0

## 1.4.0

## What's Changed
* (MINOR): Add IPatchClassBuilder, with PatchClassBuilder delegating to
an internal RoslynPatchClassBuilder by @​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#133


**Full Changelog**:
PaulTrampert/PTrampert.SimplePatch@v1.3.8...v1.4.0

## 1.3.8

## What's Changed
* (PATCH): Add an internal Reflection.Emit patch class builder by
@​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#130


**Full Changelog**:
PaulTrampert/PTrampert.SimplePatch@v1.3.7...v1.3.8

## 1.3.7

## What's Changed
* (PATCH): Bump the csharp-deps group with 6 updates by
@​dependabot[bot] in
PaulTrampert/PTrampert.SimplePatch#81
* (PATCH): Add AGENTS.md, CLAUDE.md symlink and /implement-unblocked by
@​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#125
* (PATCH): Extract the patch class model from PatchClassBuilder by
@​PaulTrampert in
PaulTrampert/PTrampert.SimplePatch#129


**Full Changelog**:
PaulTrampert/PTrampert.SimplePatch@v1.3.6...v1.3.7

Commits viewable in [compare
view](PaulTrampert/PTrampert.SimplePatch@v1.3.6...v2.0.3).
</details>

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.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.

Extract the patch class model from PatchClassBuilder so builders can share it

1 participant