Reset the cached full constructor when a type's identity changes - #11737
Conversation
`ModelProvider.BuildConstructors` returns the cached `FullConstructor` instance as part of the constructor list. `TypeProvider.Reset` clears `_constructors` and `_fullConstructor` together, but `ResetMembersBasedOnIdentityChange` (reached from `Update(name:)` and `Update(@namespace:)`) cleared only `_constructors`. That left the stale `FullConstructor` instance in the rebuilt constructor list. Generators that post-process the constructors returned from `BuildConstructors` then re-applied their mutations to the same instance. `ScmModelProvider.BuildConstructors` does exactly this for dynamic models: it appends `_patch = patch;` and `SetPropagators(...)` to the body and prepends an SCME0001 suppression, so a rebuild emitted those statements and the surrounding `#pragma` directives twice. Add a `ResetCachedConstructors` hook on `TypeProvider`, called wherever the constructor list is invalidated on an identity change, and override it in `ModelProvider` to clear `_fullConstructor`. This makes the identity-change path consistent with `Reset`. Regenerating every test project produces no output changes, so this only affects the duplicated-member case. Fixes the generated-code duplication reported in Azure/azure-sdk-for-net#61851. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa3d7684-f4ce-4618-94eb-63c1254173ca
commit: |
There was a problem hiding this comment.
Pull request overview
Synchronizes cached full constructors with constructor-list invalidation during model identity changes.
Changes:
- Adds a constructor-cache reset hook to
TypeProvider. - Clears
ModelProvider’s cachedFullConstructoron rename/namespace updates. - Adds regression tests for stale instances and duplicate mutations.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs | Updated as part of this pull request. |
| packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs | Updated as part of this pull request. |
| packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs | Updated as part of this pull request. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
No changes needing a change description found. |
|
Could we avoid introducing the private (string Name, string Namespace)? _fullConstructorIdentity;
public ConstructorProvider FullConstructor
{
get
{
var identity = (Type.Name, Type.Namespace);
if (_fullConstructor is null || _fullConstructorIdentity != identity)
{
_fullConstructor = BuildFullConstructor();
_fullConstructorIdentity = identity;
}
return _fullConstructor;
}
}This would keep invalidation local to the cache owner while preserving the selective identity-reset behavior. Will this work?
|
Keeps the fix out of the public extensibility surface: only ModelProvider, in the same assembly, needs to override it. Matches the existing private protected virtual ShouldUseFullConstructorInDerivedTypes on ModelProvider. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa3d7684-f4ce-4618-94eb-63c1254173ca
|
Updated based on review feedback: No new public surface. Why not just call Full suite is green with the current change: --generated by Copilot |
I thought the issue was the an Update call in a downstream emitter was not updating the full ctor. Won't my proposal solve that if either Name or Ns changes ? 🤔 |
`ModelProvider.BuildConstructors` returns the cached `FullConstructor` instance as part of the constructor list, so `Constructors` contains `_fullConstructor`. `TypeProvider.Reset` clears both, but the identity-change path (`Update(name:)` / `Update(@namespace:)`) cleared only `_constructors`, leaving the stale instance to be picked back up on the next rebuild. Derived providers may mutate the constructors they build. `ScmModelProvider` appends JSON patch statements and prepends an SCME0001 suppression inside `BuildConstructors`, so re-running it against the same instance applies those mutations a second time, emitting duplicated statements and duplicated `#pragma` directives in generated code. Make `FullConstructor` track the type identity it was built for and rebuild when that identity no longer matches, so the cache invalidates itself regardless of which path changes the name or namespace. Fixes duplicated constructor members reported downstream in Azure/azure-sdk-for-net#61851. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa3d7684-f4ce-4618-94eb-63c1254173ca
|
You're right on both counts — I've switched the PR to your approach. To your follow-up: yes, your proposal solves the reported case. The downstream The one thing I wanted to check before dropping the hook was a decoupling concern. My version invalidated That window turns out not to be reachable:
Given that, your version is strictly better: no new API, the invalidation lives with the cache that owns it rather than making Changes from your sketch: I also clear Full suite green: 1916 core, 1575 ClientModel, 177 Input, 55 Local, Spector 0 failed. Regenerating all test projects to reconfirm zero output drift. --generated by Copilot |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs:253
- Comparing only the resulting name/namespace does not cover every constructor-list invalidation.
TypeProvider.ResetMembersBasedOnIdentityChangeunconditionally clears_constructorsfor anyUpdate(name: ...)/Update(@namespace: ...), even when the supplied value is unchanged (or a customization maps it back to the same identity). This getter then reuses the already post-processed_fullConstructor, so a subsequentScmModelProvider.BuildConstructorscan append its suppression andSetPropagatorsstatements again. Invalidate this cache from the same reset path (for example via the virtual hook described in the PR) rather than inferring invalidation only from the tuple.
if (_fullConstructor is null || _fullConstructorIdentity != identity)
{
_fullConstructor = BuildFullConstructor();
_fullConstructorIdentity = identity;
}
|
Correction to my earlier analysis: the identity-based invalidation handles real name/namespace changes, but it misses same-value identity updates. TypeProvider.Update(name: provider.Name) still clears the Constructors cache; because the identity tuple is unchanged, FullConstructor returns the previously mutated instance and derived BuildConstructors implementations can apply their mutations again. The management generator reaches this path through visitors that reassert an already-final namespace. I reproduced the duplicate JsonPatch assignments and pragmas against the published 20260821.2 build and opened #11745 to invalidate FullConstructor whenever the constructor list is invalidated. Its full CI is green. --generated by Copilot |
Problem
ModelProvider.BuildConstructorsreturns the cachedFullConstructorinstance as part of the constructor list.TypeProvider.Resetclears_constructorsand_fullConstructortogether, butResetMembersBasedOnIdentityChange— reached fromUpdate(name:)andUpdate(@namespace:)— cleared only_constructors:_constructors_fullConstructorTypeProvider.Reset()ModelProvider.Reset)ResetMembersBasedOnIdentityChangeSo a rename or namespace change left the stale
FullConstructorinstance in the rebuilt constructor list. Generators that post-process the constructors returned fromBuildConstructorsthen re-applied their mutations to that same instance.ScmModelProvider.BuildConstructorsdoes exactly this for dynamic models — it appends_patch = patch;andSetPropagators(...)to the body and prepends anSCME0001suppression — so a rebuild emitted them twice:Fix
Add a
ResetCachedConstructorshook onTypeProvider, called wherever the constructor list is invalidated on an identity change, and override it inModelProviderto clear_fullConstructor. This makes the identity-change path consistent withReset.Validation
Microsoft.TypeSpec.Generator.Tests1916/1916,Microsoft.TypeSpec.Generator.ClientModel.Tests1575/1575,Microsoft.TypeSpec.Generator.Input.Tests177/177,TestProjects.Local.Tests55/55. The 24TestProjects.Spector.Testsfailures reproduce identically on a cleanmaincheckout (they need the Spector mock server) and are unrelated.ModelProviderTests; both fail without the fix:TestUpdate_ResetsFullConstructor— asserts the invariant directly: after an identity change,Constructorscontains the rebuiltFullConstructorand no longer contains the stale one.TestUpdate_DoesNotReapplyConstructorMutationsAfterIdentityChange— reproduces the symptom via aModelProvidersubclass that mimics howScmModelProviderpost-processes the constructor list.I also audited every site that caches or compares a
FullConstructorinstance, since replacing it is the main risk of this change:ScmModelProvidermutates it only from insideBuildConstructors, so a rebuild re-applies the mutation to a fresh instance — idempotent by construction.MrwSerializationTypeDefinitioncaches it in_serializationConstructor, but_serializationProvidersis cleared in the same reset method, so the serialization provider is rebuilt alongside it.ModelFactoryProviderreads it live within a single call, and relies onConstructors.Contains(FullConstructor)— an invariant this change restores rather than breaks.Context
Found while fixing Azure/azure-sdk-for-net#61851, where the duplication showed up in generated Azure management-plane models.