Bug
ModelProvider.BaseType (a CSharpType) and ModelProvider.BaseModelProvider (a ModelProvider) can return mismatched/inconsistent values when an emitter overrides BuildBaseType() but does not (and cannot) override the provider chain.
BuildBaseType() is virtual and emitters routinely override it to redirect the C# base class (e.g., to a framework type they own).
BuildBaseModelProvider() and the BaseModelProvider getter are not virtual, so the provider keeps walking the original InputModelType.BaseModel. Downstream code that uses BaseModelProvider to traverse the inheritance chain (e.g., model.BaseModelProvider.Properties) now diverges from the C# class hierarchy that will actually be emitted.
Repro
In an emitter, derive from ModelProvider and override the base type:
internal class MyModelProvider : ModelProvider
{
public MyModelProvider(InputModelType inputModel) : base(inputModel) { }
protected override CSharpType? BuildBaseType()
=> new CSharpType(typeof(MyFrameworkBase));
}
Now for an InputModelType whose BaseModel is set to some Parent:
provider.BaseType returns MyFrameworkBase ✅ (overridden)
provider.BaseModelProvider returns the ModelProvider for Parent ❌ (still derived from InputModel)
Any visitor that walks BaseModelProvider to enumerate inherited properties will see Parent's properties as if they were inherited, even though the generated class doesn't inherit from Parent.
Real-world impact
We hit this in the Azure provisioning generator (http-client-csharp-provisioning), which derives every model from ProvisionableConstruct regardless of the TypeSpec base. The mgmt generator's FlattenPropertyVisitor (and its PropertyHelpers.GetAllProperties helper) walks BaseModelProvider to collect inherited properties — producing duplicate flattened properties on the resource because the same properties are seen both as "inherited" (from the still-present base provider) and "own" (from the derived model where the emitter explicitly inlined them).
Expected
BaseType and BaseModelProvider should always be in agreement, by construction:
- Either
BaseModelProvider/BuildBaseModelProvider should be virtual so emitters can override them alongside BuildBaseType,
- or
BaseModelProvider should be derived from the (possibly overridden) BaseType instead of being computed independently from the input model.
Workarounds today
- Patch the input model to clear
BaseModel before constructing the provider (mutates inputs, fragile).
- Add dedup logic in every helper that walks the base chain (must be done in every emitter / visitor that does this).
- Change the spec to avoid
extends (only works when the spec author cooperates).
None of these are great — the API contract should make this case impossible.
Bug
ModelProvider.BaseType(aCSharpType) andModelProvider.BaseModelProvider(aModelProvider) can return mismatched/inconsistent values when an emitter overridesBuildBaseType()but does not (and cannot) override the provider chain.BuildBaseType()isvirtualand emitters routinely override it to redirect the C# base class (e.g., to a framework type they own).BuildBaseModelProvider()and theBaseModelProvidergetter are not virtual, so the provider keeps walking the originalInputModelType.BaseModel. Downstream code that usesBaseModelProviderto traverse the inheritance chain (e.g.,model.BaseModelProvider.Properties) now diverges from the C# class hierarchy that will actually be emitted.Repro
In an emitter, derive from
ModelProviderand override the base type:Now for an
InputModelTypewhoseBaseModelis set to someParent:provider.BaseTypereturnsMyFrameworkBase✅ (overridden)provider.BaseModelProviderreturns theModelProviderforParent❌ (still derived from InputModel)Any visitor that walks
BaseModelProviderto enumerate inherited properties will seeParent's properties as if they were inherited, even though the generated class doesn't inherit fromParent.Real-world impact
We hit this in the Azure provisioning generator (
http-client-csharp-provisioning), which derives every model fromProvisionableConstructregardless of the TypeSpec base. The mgmt generator'sFlattenPropertyVisitor(and itsPropertyHelpers.GetAllPropertieshelper) walksBaseModelProviderto collect inherited properties — producing duplicate flattened properties on the resource because the same properties are seen both as "inherited" (from the still-present base provider) and "own" (from the derived model where the emitter explicitly inlined them).Expected
BaseTypeandBaseModelProvidershould always be in agreement, by construction:BaseModelProvider/BuildBaseModelProvidershould be virtual so emitters can override them alongsideBuildBaseType,BaseModelProvidershould be derived from the (possibly overridden)BaseTypeinstead of being computed independently from the input model.Workarounds today
BaseModelbefore constructing the provider (mutates inputs, fragile).extends(only works when the spec author cooperates).None of these are great — the API contract should make this case impossible.