Description
ModelProvider's base constructor synchronously triggers a virtual call chain that ends up invoking BuildBaseType() (and other Build* overrides) on the derived class before the derived constructor body has executed. This makes it impossible for extension authors to safely override BuildBaseType if the override needs to read any state initialized in the derived constructor.
This violates the well-known .NET guideline CA2214: Do not call overridable methods in constructors.
Invocation chain
new MyModelProvider(inputModel)
└─ base(inputModel) // ModelProvider..ctor
└─ CodeModelGenerator.AddTypeToKeep(this)
└─ this.Type (TypeProvider.get_Type)
└─ this.BaseType (TypeProvider.get_BaseType)
└─ virtual BuildBaseType() // ← dispatches to DERIVED override
// while derived ctor body has NOT run yet
Any field the derived class assigns after : base(inputModel) (e.g. _inputModel = inputModel;) is still null at this point, so the override NREs.
Reproduction
A minimal extension that overrides BuildBaseType to read a derived field crashes during construction:
internal class MyProvider : ModelProvider
{
private readonly InputModelType _inputModel;
public MyProvider(InputModelType inputModel) : base(inputModel)
{
_inputModel = inputModel; // runs AFTER base ctor
}
protected override CSharpType? BuildBaseType()
{
// NRE: _inputModel is null because base(...) already invoked this method
return _inputModel.DiscriminatorValue != null ? ... : null;
}
}
Real-world example from Azure.Generator.Provisioning.Providers.ProvisioningModelProvider produces:
System.NullReferenceException: Object reference not set to an instance of an object.
at Azure.Generator.Provisioning.Providers.ProvisioningModelProvider.BuildBaseType()
at Microsoft.TypeSpec.Generator.Providers.TypeProvider.get_BaseType()
at Microsoft.TypeSpec.Generator.Providers.TypeProvider.get_Type()
at Microsoft.TypeSpec.Generator.CodeModelGenerator.AddTypeToKeep(TypeProvider type, Boolean isRoot)
at Microsoft.TypeSpec.Generator.Providers.ModelProvider..ctor(InputModelType inputModel)
at Azure.Generator.Provisioning.Providers.ProvisioningModelProvider..ctor(InputModelType inputModel)
Why this matters
ModelProvider is an explicit extension point — generators (mgmt, provisioning, custom emitters) routinely subclass it and override Build* methods. The framework currently imposes an implicit, undocumented contract: "you may override BuildBaseType, but you must not reference any state your own constructor sets up." Extension authors discover this only via runtime NREs deep in framework code with no obvious connection to the real cause.
The mgmt generator avoids this only by accident — it does not read derived-class fields in its overrides.
Proposed fix
AddTypeToKeep (and anything that walks Type / BaseType / Name on this) should not run from the ModelProvider constructor. Options:
- Defer registration to a post-construction hook the framework invokes after
new returns (factory-style).
- Lazy registration on first external access — let the existing lazy evaluation of
Type trigger registration once it is actually consumed, not eagerly during construction.
- At minimum, document the constraint and consider an analyzer/diagnostic so extension authors get a build-time signal rather than a runtime NRE.
Option 1 or 2 would fully eliminate the footgun and align with CA2214.
Related
- Downstream workaround tracking: Azure/azure-sdk-for-net PR #58345 (CostManagement provisioning library) is currently blocked by this.
Description
ModelProvider's base constructor synchronously triggers a virtual call chain that ends up invokingBuildBaseType()(and otherBuild*overrides) on the derived class before the derived constructor body has executed. This makes it impossible for extension authors to safely overrideBuildBaseTypeif the override needs to read any state initialized in the derived constructor.This violates the well-known .NET guideline CA2214: Do not call overridable methods in constructors.
Invocation chain
Any field the derived class assigns after
: base(inputModel)(e.g._inputModel = inputModel;) is stillnullat this point, so the override NREs.Reproduction
A minimal extension that overrides
BuildBaseTypeto read a derived field crashes during construction:Real-world example from
Azure.Generator.Provisioning.Providers.ProvisioningModelProviderproduces:Why this matters
ModelProvideris an explicit extension point — generators (mgmt, provisioning, custom emitters) routinely subclass it and overrideBuild*methods. The framework currently imposes an implicit, undocumented contract: "you may overrideBuildBaseType, but you must not reference any state your own constructor sets up." Extension authors discover this only via runtime NREs deep in framework code with no obvious connection to the real cause.The mgmt generator avoids this only by accident — it does not read derived-class fields in its overrides.
Proposed fix
AddTypeToKeep(and anything that walksType/BaseType/Nameonthis) should not run from theModelProviderconstructor. Options:newreturns (factory-style).Typetrigger registration once it is actually consumed, not eagerly during construction.Option 1 or 2 would fully eliminate the footgun and align with CA2214.
Related