Skip to content

(PATCH): Drop the Roslyn patch class builder's own cache - #154

Merged
PaulTrampert merged 3 commits into
mainfrom
feature/150-drop-roslyn-builder-cache
Oct 5, 2026
Merged

PaulTrampert merged 3 commits into
mainfrom
feature/150-drop-roslyn-builder-cache

Conversation

@PaulTrampert

Copy link
Copy Markdown
Owner

RoslynPatchClassBuilder isn't used at runtime (it's kept pending #144). Now that CachingPatchClassBuilder exists (#148), the Roslyn builder no longer keeps a cache of its own.

Changes

  • RoslynPatchClassBuilder: removed the static OptionalsClasses dictionary and the comments about it. GetPatchClassFor now compiles and loads a new assembly on every call, and its doc comment says so and points to CachingPatchClassBuilder. Instance stays, and its summary now says the builder holds no state, rather than mentioning a static cache.
  • PatchClassBuilders (the fixture source for the shared PatchClassBuilderTest suite) wraps each builder in a CachingPatchClassBuilder. Without that, the Roslyn fixture would compile a class on every GetPatchClassFor call and the suite would slow down. The suite still tests what each builder produces, because the cache only returns what the inner builder built. The wrappers are static fields, so every time NUnit enumerates All() it gets the same two caches. The Emit builder is wrapped too, which is harmless while it still has its own cache (Route PatchClassBuilder.Instance through CachingPatchClassBuilder and drop the Emit builder's cache #149 removes that).
  • RoslynPatchClassBuilderTest: removed the two cache tests (same type on repeat calls, one compile under concurrent first use). CachingPatchClassBuilderTest covers that behaviour. Added GetPatchClassFor_BuildsANewClassOnEachCall, which fails before the fix and passes after it.

ConcurrentFirstUseTestObject stays because EmitPatchClassBuilderTest still uses it. A comment in EmitPatchClassBuilder still mentions "RoslynPatchClassBuilder's cache". I left it alone so this PR doesn't touch the Emit builder, since #149 rewrites that part.

No public API change.

Test results

  • dotnet build: succeeds, with 21 unique warnings both before and after this change.
  • dotnet test: all passing. Core: 131. OpenApi: 12. Swashbuckle: 18.
  • The new regression test fails on main, because the second call returned the same cached type.

Closes #150

🤖 Generated with Claude Code

PaulTrampert and others added 2 commits October 4, 2026 18:21
Lets the Roslyn builder drop its own cache without the shared suite
compiling a new class on every call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetPatchClassFor now compiles a new class on every call; caching is
CachingPatchClassBuilder's job. Replaces the cache tests with one that
checks each call builds a new class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
public Type GetPatchClassFor(Type type)
{
return OptionalsClasses.GetOrAdd(type, t => new Lazy<Type>(() => CreatePatchClass(t))).Value;
return CreatePatchClass(type);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude Since this is just a call through to the private method, we can do away with the private method.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 77b70dd: the body of CreatePatchClass now lives in GetPatchClassFor, and the private method is gone. Build warnings unchanged; all tests pass (131/12/18).

PaulTrampert added a commit that referenced this pull request Oct 4, 2026
…ilder (#155)

## What changes

`PatchClassBuilder.Instance` now holds one `CachingPatchClassBuilder`
around `EmitPatchClassBuilder.Instance`. The Emit builder no longer has
a static cache, so it emits a new class in a new dynamic assembly on
every call. The two changes go in one PR, as #149 says: either one alone
would leave the runtime path with no cache, or with two.

Things the diff doesn't make obvious:

- **`Instance` is `{ get; } = new ...`, not `=> new ...`.** An
expression body would create a decorator with an empty cache on every
call, so every call would emit a new assembly.
`Instance_IsTheSameBuilderEachTime` guards this.
- **`CachingPatchClassBuilder` gets an `Inner` property** (the class is
internal, so the public API doesn't change). It lets
`PatchClassBuilderDelegationTest` check that `Instance` wraps the Emit
builder, as the issue asks. A behavioural check, such as looking for the
`PTrampert.SimplePatch.Emitted` assembly name, was the other option. I
rejected it because it wouldn't tell "wraps the Emit builder" apart from
"is the Emit builder". The decorator's lambda now reads `Inner` instead
of the captured parameter, so the class holds a single field rather than
two copies of the inner builder.
- **The Emit cache tests moved.** These are same type on repeat, a
single build under concurrent first use, and the count of emitted
dynamic assemblies. They now run through `PatchClassBuilder.Instance` in
`PatchClassBuilderDelegationTest`. `EmitPatchClassBuilderTest` keeps
only the tests that are specific to Emit.
- The comment in `EmitPatchClassBuilder` that mentioned
`RoslynPatchClassBuilder`'s cache is gone along with the dictionary, so
it doesn't conflict with #154 (#150). This PR doesn't touch the Roslyn
builder or `PatchClassBuilders.cs`.
- The "How it works" bullet in `AGENTS.md` now describes the decorator.

No public API change.

## Tests

- Regression: before the fix,
`Instance_IsACachingBuilderAroundTheEmitBuilder` failed (1 failed, 132
passed). It passes after the fix.
- `dotnet build`: the warning set is the same as on `main` (21 unique
warnings, none new).
- `dotnet test`: core 133/133, OpenApi 12/12, Swashbuckle 18/18.

Closes #149

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With the cache gone, GetPatchClassFor only forwarded to the private method.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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 505aadc into main Oct 5, 2026
8 checks passed
@PaulTrampert
PaulTrampert deleted the feature/150-drop-roslyn-builder-cache branch October 5, 2026 01:47
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.

Drop the Roslyn patch class builder's own cache

1 participant