Bug Report
Description
AddTypeToKeep(TypeProvider) eagerly resolves type.Type.FullyQualifiedName to a string at call time. When a visitor (e.g. NamespaceVisitor) later changes the type's namespace, the stored FQN becomes stale. PostProcessor.ShouldKeepType() then fails to match the type, causing it to be internalized even though @@access(public) was set.
This was discovered during the ProviderHub management SDK migration, where models like ManifestLevelPropertyBag and ProviderFeaturesRule were silently internalized despite explicit @@access(Access.public) decorators.
Root Cause
In CodeModelGenerator.cs:
public void AddTypeToKeep(TypeProvider type, bool isRoot = true)
=> AddTypeToKeep(type.Type.FullyQualifiedName, isRoot);
This is called from ModelProvider constructor (for Access == "public") and TypeFactory (for public enums) — both run before visitors execute.
The pipeline order in CSharpGen.ExecuteAsync():
- Build TypeProviders (constructors run →
AddTypeToKeep stores pre-visitor FQN)
- Run visitors (
NamespaceVisitor changes namespace → FQN changes)
- Write generated files
PostProcessAsync() → PostProcessor receives stale FQNs in typesToKeep
In PostProcessor.ShouldKeepType(), both checks fail:
- Simple name
"MyModel" vs typesToKeep containing "OldNamespace.MyModel" → no match
- Roslyn FQN
"NewNamespace.Models.MyModel" vs "OldNamespace.MyModel" → no match
Impact
Models with @@access(public) get silently internalized when model-namespace is true (the mgmt default). This affects management SDK migrations where models like ManifestLevelPropertyBag (ProviderHub service) become internal despite explicit public access decorators.
Reproduction
[Test]
public void PublicModelTypeToKeepUpdatesAfterNamespaceChange()
{
var inputModel = InputFactory.Model(
"MockInputModel",
access: "public");
MockHelpers.LoadMockGenerator(
inputModelTypes: [inputModel]);
var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders
.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider;
Assert.IsNotNull(modelProvider);
var originalFqn = modelProvider!.Type.FullyQualifiedName;
Assert.AreEqual("Sample.Models.MockInputModel", originalFqn);
// Simulate a visitor changing the namespace (e.g., NamespaceVisitor appending ".Models")
modelProvider.Update(@namespace: "NewNamespace.Models");
var newFqn = modelProvider.Type.FullyQualifiedName;
Assert.AreEqual("NewNamespace.Models.MockInputModel", newFqn);
// After namespace change, the resolved root types should contain the updated FQN
// so that PostProcessor.ShouldKeepType can match it against the generated code.
var rootTypes = CodeModelGenerator.Instance.AdditionalRootTypes;
Assert.IsTrue(rootTypes.Contains(newFqn),
$"AdditionalRootTypes should contain the post-visitor FQN '{newFqn}' "
+ $"but only contains: [{string.Join(", ", rootTypes)}]");
}
Proposed Fix
Store TypeProvider references instead of eagerly resolving FQNs. Resolve lazily when AdditionalRootTypes/NonRootTypes are accessed (after visitors have run).
Bug Report
Description
AddTypeToKeep(TypeProvider)eagerly resolvestype.Type.FullyQualifiedNameto a string at call time. When a visitor (e.g.NamespaceVisitor) later changes the type's namespace, the stored FQN becomes stale.PostProcessor.ShouldKeepType()then fails to match the type, causing it to be internalized even though@@access(public)was set.This was discovered during the ProviderHub management SDK migration, where models like
ManifestLevelPropertyBagandProviderFeaturesRulewere silently internalized despite explicit@@access(Access.public)decorators.Root Cause
In
CodeModelGenerator.cs:This is called from
ModelProviderconstructor (forAccess == "public") andTypeFactory(for public enums) — both run before visitors execute.The pipeline order in
CSharpGen.ExecuteAsync():AddTypeToKeepstores pre-visitor FQN)NamespaceVisitorchanges namespace → FQN changes)PostProcessAsync()→PostProcessorreceives stale FQNs intypesToKeepIn
PostProcessor.ShouldKeepType(), both checks fail:"MyModel"vs typesToKeep containing"OldNamespace.MyModel"→ no match"NewNamespace.Models.MyModel"vs"OldNamespace.MyModel"→ no matchImpact
Models with
@@access(public)get silently internalized whenmodel-namespaceis true (the mgmt default). This affects management SDK migrations where models likeManifestLevelPropertyBag(ProviderHub service) become internal despite explicit public access decorators.Reproduction
Proposed Fix
Store
TypeProviderreferences instead of eagerly resolving FQNs. Resolve lazily whenAdditionalRootTypes/NonRootTypesare accessed (after visitors have run).