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.
Summary
When
ApiCompatVersionis 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.MonitorGenerator package:
@azure-typespec/http-client-csharp-mgmt/Microsoft.TypeSpec.Generator1.0.0-alpha.20260612.4Relevant TypeSpec:
Actual behavior with ApiCompatVersion set
The generated public API has concrete classes with public constructors that expose discriminator parameters:
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
ApiCompatVersionfromsdk/monitor/Azure.ResourceManager.Monitor/src/Azure.ResourceManager.Monitor.csprojand regenerated.Without
ApiCompatVersion, both models became abstract and the discriminator constructors becameprivate protected, which matches the normal discriminator-base pattern: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: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.