microsoft / microsoft/typespec
C# generator exposes discriminator parameter on concrete model due to ApiCompatVersion
- Dominant language
- Java
- Stars
- 5.9k
- Forks
- 394
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 104
Description
## 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:
```tsp
@discriminator("odata.type")
model MetricAlertCriteria {
...Record;
#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;
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:
```csharp
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:
```csharp
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:
```csharp
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.
Contributor guide
Assessment
This issue has not been assessed yet.