Skip to content

[csharp] ModelProvider.BaseType and BaseModelProvider can return mismatched values #10485

@ArcturusZhang

Description

@ArcturusZhang

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    emitter:client:csharpIssue for the C# client emitter: @typespec/http-client-csharp

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions