Skip to content

ModelProvider base constructor invokes virtual BuildBaseType (via AddTypeToKeep) before derived ctor runs #10626

@ArcturusZhang

Description

@ArcturusZhang

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:

  1. Defer registration to a post-construction hook the framework invokes after new returns (factory-style).
  2. Lazy registration on first external access — let the existing lazy evaluation of Type trigger registration once it is actually consumed, not eagerly during construction.
  3. 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.

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