Skip to content

C# generator exposes discriminator parameter on concrete model due to ApiCompatVersion #10996

@ArcturusZhang

Description

@ArcturusZhang

Summary

When ApiCompatVersion is set, the C# generator appears to preserve a discriminator base model as concrete to match the previous contract, but it still includes the required discriminator property as a public constructor parameter.

This exposes a discriminator value in the public API even though the discriminator property itself is internal.

Repro context

Repository/package: Azure.ResourceManager.Monitor
Generator package: @azure-typespec/http-client-csharp-mgmt / Microsoft.TypeSpec.Generator 1.0.0-alpha.20260612.4

Relevant TypeSpec:

@discriminator("odata.type")
model MetricAlertCriteria {
  ...Record<unknown>;

  #suppress "@azure-tools/typespec-azure-core/casing-style" "FIXME: Update justification, follow aka.ms/tsp/conversion-fix for details"
  `odata.type`: Odatatype;
}

@discriminator("criterionType")
model MultiMetricCriteria {
  ...Record<unknown>;

  criterionType: CriterionType;
  name: string;
  metricName: string;
  metricNamespace?: string;
  timeAggregation: AggregationTypeEnum;
  @identifiers(#["name"])
  dimensions?: MetricDimension[];
  skipMetricValidation?: boolean;
}

Actual behavior with ApiCompatVersion set

The generated public API has concrete classes with public constructors that expose discriminator parameters:

public partial class MetricAlertCriteria
{
    public MetricAlertCriteria(Odatatype odataType) { ... }
    internal Odatatype OdataType { get; set; }
}

public partial class MultiMetricCriteria
{
    public MultiMetricCriteria(
        CriterionType criterionType,
        string name,
        string metricName,
        MetricCriteriaTimeAggregationType timeAggregation) { ... }

    internal CriterionType CriterionType { get; set; }
}

The discriminator property is correctly not public, but its value is still required as a public constructor parameter.

Expected behavior

Discriminator values should not appear in public model constructors. If the generator makes the discriminator base concrete for back-compat, it should still avoid exposing discriminator parameters in public constructors, likely by restoring/keeping the previous public constructor shape and handling discriminator defaults internally.

Evidence this is tied to ApiCompat/back-compat behavior

As an experiment, I temporarily removed ApiCompatVersion from sdk/monitor/Azure.ResourceManager.Monitor/src/Azure.ResourceManager.Monitor.csproj and regenerated.

Without ApiCompatVersion, both models became abstract and the discriminator constructors became private protected, which matches the normal discriminator-base pattern:

public abstract partial class MetricAlertCriteria
{
    private protected MetricAlertCriteria(Odatatype odataType) { ... }
    internal Odatatype OdataType { get; set; }
}

public abstract partial class MultiMetricCriteria
{
    private protected MultiMetricCriteria(
        CriterionType criterionType,
        string name,
        string metricName,
        MetricCriteriaTimeAggregationType timeAggregation) { ... }

    internal CriterionType CriterionType { get; set; }
}

This suggests the back-compat feature reads the previous contract, sees the model was non-abstract, preserves concreteness, and unintentionally exposes required discriminator parameters in the public constructor.

Related normal case

The existing generator test/project for a normal discriminator base (Plant) generates an abstract base model:

public abstract partial class Plant
{
    private protected Plant(string species, string id, int height) { ... }
    internal string Species { get; set; }
}

That does not expose the discriminator in public API because the base model remains abstract. The issue appears specific to concrete discriminator bases produced/preserved for back compatibility.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingemitter:client:csharpIssue for the C# client emitter: @typespec/http-client-csharp

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions