Skip to content

[MTG] Preserve TypeSpec base properties when a custom CLR base is narrower #11727

Description

@live1206

Description

When custom C# code changes a generated model to inherit from a CLR base that is narrower than the model's TypeSpec base, properties supplied only by the original TypeSpec base disappear instead of being generated directly on the derived model.

This was initially observed in the Azure management-plane generator, but the underlying asymmetry is in MTG's model construction and applies generically to custom base overrides.

Azure SDK issue: Azure/azure-sdk-for-net#62140

Example

The TypeSpec model is a tracked resource, so its TypeSpec base contributes properties such as location and tags.

Custom code preserves an existing shipped CLR hierarchy:

public partial class BatchAccountData : ResourceData
{
}

ResourceData is narrower than TrackedResourceData: it does not provide Location or Tags.

The generated partial correctly changes its CLR base to ResourceData, but Location and Tags disappear from:

  • the generated model surface
  • constructors
  • serialization and deserialization
  • model factory generation

The expected result is for Location and Tags to be generated directly on BatchAccountData because they remain part of the TypeSpec model but are no longer supplied by the effective CLR base.

Root cause

MTG correctly gives CustomCodeView.BaseType precedence when selecting the generated CLR base:

  • ModelProvider.BuildBaseType() returns the custom base
  • ModelProvider.BuildBaseModelProvider() resolves the custom base provider instead of the original TypeSpec base provider

However, the property-building pipeline then only has two sources:

  1. _inputModel.Properties, which contains properties directly declared by the derived TypeSpec model
  2. EnumerateBaseModelProviders(), which now follows the effective custom CLR base hierarchy

Properties declared by the original TypeSpec base remain owned by _inputModel.BaseModel. Once the effective BaseModelProvider is replaced by the custom CLR base, that original TypeSpec hierarchy is disconnected from model generation.

As a result, properties that are:

  • declared on the original TypeSpec base, and
  • not provided by the effective custom CLR base

are neither inherited nor generated directly on the derived model.

This is an asymmetry:

  • A broader custom CLR base works because MTG can filter properties already supplied by the effective base.
  • A narrower custom CLR base loses properties because MTG has no reconciliation step for properties supplied only by the original TypeSpec base.

Why a post-generation visitor fix is unsafe

An attempted management-generator fix reconciled properties in a visitor after ModelProvider.Properties had already been built. A full Azure SDK regeneration demonstrated that this approach is too late and too broad:

  • 183 generated models gained duplicate/generic Type properties
    • TypeSpec type was compared with C# ResourceType
    • TypeSpec discriminator type was compared with names such as ActivityType or CopySourceType
  • a model gained a duplicate ETag property because input etag was compared with generated ETag
  • inherited discriminator properties were materialized again on many polymorphic Data Factory models
  • the intended Location and Tags properties appeared on two models, but constructors and serialization were not updated

This happens because a visitor only sees already-created PropertyProviders while constructors and serialization continue to derive from the original input model/hierarchy. Comparing raw InputModelProperty.Name with generated C# property names also misses renames and framework mappings.

Full regen PR demonstrating the impact: Azure/azure-sdk-for-net#62195

Expected MTG behavior

MTG should reconcile the original TypeSpec hierarchy with the effective CLR hierarchy before building any model artifacts.

Conceptually:

  1. Preserve the original TypeSpec base chain even when a custom CLR base wins.
  2. Determine which original TypeSpec base properties are already represented by the effective CLR base.
  3. Generate the remaining TypeSpec-inherited properties directly on the derived model.
  4. Use that same reconciled property plan for:
    • properties and fields
    • public and internal constructors
    • serialization/deserialization
    • model factories
  5. Match properties semantically rather than by raw names, accounting for:
    • serialized/wire name
    • C# renames such as type to ResourceType
    • discriminator renames
    • system/external model mappings
    • visibility and usage
    • ShouldSkipDerivedModelProperties

Suggested regression coverage

Add an MTG model customization test with:

  • a TypeSpec base model containing properties supplied by both a broad base and a narrower base
  • a derived TypeSpec model whose custom partial selects the narrower CLR base
  • one inherited property already represented by the custom CLR base under a different C# name (for example type / ResourceType)
  • properties not represented by the custom base (for example location and tags)
  • a renamed discriminator in the original base hierarchy

Verify that:

  • the custom CLR base wins
  • properties already supplied by the custom base are not duplicated
  • missing TypeSpec-base properties are generated on the derived model
  • constructors include those properties when appropriate
  • serialization and deserialization include those properties
  • discriminators are not duplicated under a generic Type property

Metadata

Metadata

Labels

bugSomething isn't workingemitter: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