Azure / Azure/azure-sdk-for-net
[Mgmt Generator] Custom base model support does not preserve discriminator hierarchy API shape
- Dominant language
- C#
- Stars
- 6.1k
- Forks
- 5.2k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 407
Description
## Description
During the `Azure.ResourceManager.SecurityCenter` MPG migration, we tried to replace legacy C# `@@hierarchyBuilding` / replacement-model `@@alternateType` workarounds with SDK-side custom base partial classes after the recent generator custom-base support.
That works for normal resource data models, but it does not work for a discriminated hierarchy that also needs to preserve an older model API shape. The generated discriminator base redeclares members already inherited from the custom base, and attempts to suppress those members can corrupt generated derived serialization/constructor code.
SecurityCenter currently has to keep replacement models for `ExternalSecuritySolution` / `CefExternalSecuritySolution` / `AtaExternalSecuritySolution` / `AadExternalSecuritySolution` because SDK custom-base code alone cannot preserve the old API surface.
## Minimal TypeSpec repro
```typespec
import "@azure-tools/typespec-azure-core";
import "@azure-tools/typespec-azure-resource-manager";
import "@typespec/http";
import "@typespec/rest";
using Azure.Core;
using Azure.ResourceManager;
using TypeSpec.Http;
using TypeSpec.Rest;
@armProviderNamespace
namespace Microsoft.Repro;
@service(#{ title: "Repro" })
@versioned(Versions)
namespace Repro;
enum Versions {
v2024_01_01: "2024-01-01",
}
@doc("External solution kind.")
enum ExternalSolutionKind {
CEF,
ATA,
}
@doc("Base discriminated model/resource data shape.")
@discriminator("kind")
model ExternalSolution is ProxyResource<{}, false> {
...ResourceNameParameter<
Resource = ExternalSolution,
KeyName = "solutionName",
SegmentName = "externalSolutions"
>;
@visibility(Lifecycle.Read)
location?: string;
kind: ExternalSolutionKind;
}
@doc("CEF derived shape.")
model CefExternalSolution extends ExternalSolution {
kind: "CEF";
properties?: CefSolutionProperties;
}
@doc("ATA derived shape.")
model AtaExternalSolution extends ExternalSolution {
kind: "ATA";
properties?: AtaSolutionProperties;
}
model CefSolutionProperties {
workspaceId?: string;
}
model AtaSolutionProperties {
workspaceId?: string;
}
@parentResource(ArmLocationResource)
model ReproParent is ProxyResource<{}, false> {
...ResourceNameParameter<
Resource = ReproParent,
KeyName = "parentName",
SegmentName = "reproParents"
>;
}
@armResourceOperations
interface ExternalSolutions {
get is ArmResourceRead;
listByParent is ArmResourceListByParent<
ExternalSolution,
Response = ArmResponse<{
@pageItems
value?: ExternalSolution[];
@nextLink
nextLink?: string;
}>
>;
}
```
Then add SDK custom code intended to preserve the old model API surface:
```csharp
// Custom/Models/ExternalSolution.cs
namespace Azure.ResourceManager.Repro.Models
{
public partial class ExternalSolution : Azure.ResourceManager.Models.ResourceData
{
public ExternalSolution()
{
}
public ExternalSolutionKind? Kind { get; set; }
public Azure.Core.AzureLocation? Location { get; }
}
}
```
```csharp
// Custom/ExternalSolutionData.cs
namespace Azure.ResourceManager.Repro
{
public abstract partial class ExternalSolutionData : Models.ExternalSolution
{
}
}
```
## Expected behavior
The generator should allow the generated discriminated base data type to use the SDK custom base without producing duplicate/hiding members, so the generated derived types remain assignable to the legacy base model API.
Possible acceptable generator behavior:
- Detect that custom base already defines inherited members such as `Name`, `Location`, `Kind`, `Properties`, and serialization core methods, then skip or correctly mark generated members.
- Emit valid `new`/`override` modifiers where hiding is intentional.
- Preserve valid serialization/deserialization for derived discriminator models when custom-code members are present or suppressed.
## Actual behavior observed in SecurityCenter
With a custom partial like:
```csharp
public abstract partial class ExternalSecuritySolutionData : Models.ExternalSecuritySolution
{
}
```
regeneration succeeds, but a normal build fails because warnings are treated as errors:
```text
CS0108: 'ExternalSecuritySolutionData.Name' hides inherited member 'ResourceData.Name'. Use the new keyword if hiding was intended.
CS0108: 'ExternalSecuritySolutionData.Properties' hides inherited member 'ExternalSecuritySolution.Properties'. Use the new keyword if hiding was intended.
CS0108: 'ExternalSecuritySolutionData.Kind' hides inherited member 'ExternalSecuritySolution.Kind'. Use the new keyword if hiding was intended.
CS0108: 'ExternalSecuritySolutionData.Location' hides inherited member 'ExternalSecuritySolution.Location'. Use the new keyword if hiding was intended.
CS0114: 'ExternalSecuritySolutionData.JsonModelWriteCore(...)' hides inherited member 'ResourceData.JsonModelWriteCore(...)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
```
Building with `TreatWarningsAsErrors=false` gets past compilation, but ApiCompat still shows that the old discriminator model API is not preserved unless replacement models remain.
We also tried SDK custom-code suppression of the colliding generated members, but that produced invalid generated derived code in SecurityCenter, including missing synthetic locals such as `properties0` and malformed `global::.BinaryData` references in generated constructors/serialization.
## Why this matters
The latest generator custom-base support lets us remove `@@hierarchyBuilding` workarounds for simple model/resource base changes, but discriminator hierarchies that need to preserve a legacy model API still require replacement-model `@@alternateType` workarounds. This blocks fully removing SecurityCenter's C# replacement models for the external security solution hierarchy.
## Current workaround
Keep C# replacement models and `@@alternateType` mappings for the discriminator hierarchy:
- `ExternalSecuritySolution`
- `CefExternalSecuritySolution`
- `AtaExternalSecuritySolution`
- `AadExternalSecuritySolution`
while using SDK custom base partial classes for the simpler non-discriminator cases.
Contributor guide
Assessment
This issue has not been assessed yet.