Skip to content

[csharp] ModelFactoryProvider.GetParameters() preserves constructor order, breaking backward-compat overloads with @@hierarchyBuilding #10397

@live1206

Description

@live1206

Bug: ModelFactoryProvider.GetParameters() preserves constructor parameter order, causing backward-compat overloads to emit wrong argument order

Description

When @@hierarchyBuilding is applied to a model to change its base type (e.g., restoring TrackedResourceData during an AutoRest → TypeSpec migration), the generated backward-compat ModelFactory overload passes arguments in the internal constructor parameter order instead of the public method parameter order, causing compile errors.

Root Cause

ModelFactoryProvider.GetParameters() builds the primary ModelFactory method's parameters by iterating fullConstructor.Signature.Parameters, preserving constructor parameter order:

// In ModelFactoryProvider.BuildMethods():
MethodSignature methodSignature = new MethodSignature(
    modelProvider.Name, ..., 
    GetParameters(modelProvider, item));  // item = fullConstructor

// GetParameters iterates constructor params in constructor order:
private static IReadOnlyList<ParameterProvider> GetParameters(
    ModelProvider modelProvider, ConstructorProvider fullConstructor)
{
    IReadOnlyList<ParameterProvider> parameters = fullConstructor.Signature.Parameters;
    foreach (ParameterProvider item in parameters)
    {
        list.Add(GetModelFactoryParam(item));  // preserves constructor order
    }
}

When @@hierarchyBuilding remaps a model to TrackedResource, InheritableSystemObjectModelVisitor reorders the internal constructor to: (resourceType, systemData, additionalBinaryDataProperties, location, id, name, tags, properties). This differs from the standard public API order (id, name, resourceType, systemData, tags, location, ...).

The primary ModelFactory method gets parameters in this constructor order. Then BuildMethodsForBackCompatibilityTryBuildMethodArgumentsForOverload iterates currentMethod.Parameters (constructor-ordered) and emits positional arguments. But the backward-compat overload declares parameters in the previous contract order, so the positional args don't match.

Reproduction

Apply @@hierarchyBuilding to a non-ARM Patch model that has id, name, type, location, tags properties, where a previous API contract exists with a ModelFactory method:

TypeSpec model:

model CapacityPoolPatch {
  location?: string;
  @visibility(Lifecycle.Read) id?: string;
  @visibility(Lifecycle.Read) name?: string;
  @visibility(Lifecycle.Read) type?: string;
  tags?: Record<string>;
  properties?: PatchProperties;
}

model PatchProperties {
  size?: int64;
  qosType?: string;
  coolAccess?: boolean;
  customThroughputMibpsInt?: int32;
}

client.tsp:

@@alternateType(CapacityPoolPatch.id, armResourceIdentifier, "csharp");
@@alternateType(CapacityPoolPatch.location, azureLocation, "csharp");
@@hierarchyBuilding(CapacityPoolPatch, Azure.ResourceManager.Foundations.TrackedResource, "csharp");

Expected

The backward-compat overload should use named arguments or match parameter names:

public static CapacityPoolPatch CapacityPoolPatch(
    ResourceIdentifier id, string name, ResourceType resourceType, 
    SystemData systemData, IDictionary<string, string> tags, 
    AzureLocation location, long? size, CapacityPoolQosType? qosType, 
    bool? isCoolAccessEnabled)
{
    return CapacityPoolPatch(
        id: id, name: name, resourceType: resourceType, 
        systemData: systemData, tags: tags, location: location, 
        size: size, qosType: qosType, 
        isCoolAccessEnabled: isCoolAccessEnabled, 
        customThroughputMibpsInt: default);
}

Actual

Arguments emitted in constructor order (positionally):

return CapacityPoolPatch(
    resourceType, systemData, location, id, name, tags, 
    size, qosType, isCoolAccessEnabled, 
    customThroughputMibpsInt: default);

This causes CS1503 because resourceType (ResourceType) is passed where id (ResourceIdentifier) is expected, etc.

Fix Suggestion

Either:

  1. GetParameters() should sort output parameters to match a canonical order (id, name, resourceType, systemData, tags, location, ...remaining) rather than preserving constructor parameter order
  2. TryBuildMethodArgumentsForOverload should emit named arguments when calling the current method, matching by parameter name instead of position

Impact

Blocks using @@hierarchyBuilding on Patch models during Azure SDK management-plane migration (AutoRest → TypeSpec) when the model has a backward-compat ModelFactory overload from a previous contract. Discovered during NetApp SDK migration (Azure/azure-sdk-for-net#58197).

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