Skip to content

Commit be01338

Browse files
live1206Copilot
andauthored
Add SystemObjectModelProvider for non-generated base type customization (#9862)
## Summary Fixes #9234 Adds `SystemObjectModelProvider`, a `ModelProvider` subclass for input models that map to existing framework/system model types such as `TrackedResourceData` and `ResourceData`. This lets downstream generators, especially the Azure Management generator, use non-generated framework types as model bases while still allowing the shared generator to reason about inherited properties, constructors, and serialization. ## Key changes ### `SystemObjectModelProvider` - Represents a framework/system model type using a `CSharpType` plus the `InputModelType` it replaces. - Extends `ModelProvider`, unlike `SystemObjectTypeProvider`, so it can be used as `BaseModelProvider` for generated derived models. - Exposes `SystemType` and `CrossLanguageDefinitionId` for downstream generator logic. - Uses the framework type name/namespace and never writes a generated file. - Builds properties and constructors from the input model so derived models can correctly reason about inherited constructor parameters such as `id`, `name`, `resourceType`, `systemData`, `tags`, and `location`. - Does not generate fields, serialization providers, or a raw data field because the framework type owns those implementation details. - Resets cached provider state after assigning the wrapped `CSharpType`, because the base constructor can evaluate cached type metadata before the derived field is assigned. ### Base-provider and inheritance handling - `ModelProvider.BuildBaseModelProvider()` is now `protected virtual`, so customized base-provider resolution can be specialized without adding a broad `Update(baseModelProvider)` mutation API. - Removed the `Update(baseModelProvider)` path that could create ambiguous call sites with the existing `Update(...)` method. - `BuildBaseModelProvider()` resolves customized base types through `BaseType`, including generated-model customizations and a name+namespace fallback for framework/non-framework `CSharpType` equality differences. - Added provider hooks for base-provider-specific generated behavior: - `ShouldSkipDerivedModelProperties` for framework-owned properties that should not be re-emitted by derived models. - `ShouldSkipDerivedSerializationMethodOverrides` for serialization methods that should remain virtual instead of overriding framework-owned methods. - Property generation now tracks normal base properties separately from skipped base property names. This avoids concrete `SystemObjectModelProvider` checks and preserves multi-layer inheritance behavior: properties owned by a skipped framework base are omitted, while properties owned by an intermediate generated base can still drive normal `override`/`new` behavior. - Raw-data and serialization behavior now accounts for `SystemObjectModelProvider` bases through provider hooks instead of concrete type checks in common paths. ### Constructor initializer fix - Fixed base constructor initializer argument selection after public input parameters are substituted. This prevents generated code like `: base(location0)` when the actual constructor parameter is `location`. ## Motivation The Azure Management generator has ARM resource models that inherit from framework types such as `TrackedResourceData` and `ResourceData`. Previously it needed a local `InheritableSystemObjectModelProvider` with custom constructor and property handling. Moving this capability into the shared generator lets downstream generators reuse normal `ModelProvider` behavior for inheritance, property deduplication, serialization modifiers, and constructor generation while keeping management-specific wiring in the management generator. ## Validation TypeSpec / MTG validation: - `npm run build:generator` in `packages/http-client-csharp` passed. - `SystemObjectModelProviderTests` passed. - `SystemObjectModelSerializationTests` passed. - Fixed the CI failure in `JsonModelWriteCore_IsOverride_WhenBaseIsSystemObject` by keeping JSON write-core overrides independent from the hook that suppresses persistable/create-core overrides. Azure SDK / MPG validation with the companion PR and local `C:\src\typespec` checkout: - `eng/packages/http-client-csharp-mgmt/eng/scripts/Generate.ps1` completed successfully. - `dotnet test eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/test/Azure.Generator.Mgmt.Tests.csproj` passed: 194/194 tests. - Regenerated two real management-plane SDKs with local MTG + local MPG generator: - `Azure.ResourceManager.Advisor` - `Azure.ResourceManager.CloudHealth` - Both selected SDK regenerations completed successfully (`2 passed, 0 failed`) and both regenerated packages built successfully. ## Companion PR - SDK: Azure/azure-sdk-for-net#56634 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1e5f02d commit be01338

5 files changed

Lines changed: 757 additions & 21 deletions

File tree

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public partial class MrwSerializationTypeDefinition : TypeProvider
6666
private ConstructorProvider? _serializationConstructor;
6767
// Flag to determine if the model should override the serialization methods
6868
private readonly bool _shouldOverrideMethods;
69+
private readonly bool _shouldSkipDerivedSerializationMethodOverrides;
6970
private readonly Lazy<PropertyProvider[]> _additionalProperties;
7071

7172
private CSharpType RootType => _rootType ??= GetRootModelType();
@@ -91,6 +92,7 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m
9192
_additionalBinaryDataProperty = new(GetAdditionalBinaryDataPropertiesProp);
9293
_additionalProperties = new(() => [.. _model.Properties.Where(p => p.IsAdditionalProperties)]);
9394
_shouldOverrideMethods = _model.BaseModelProvider != null && !_isStruct;
95+
_shouldSkipDerivedSerializationMethodOverrides = _model.BaseModelProvider?.ShouldSkipDerivedSerializationMethodOverrides == true;
9496
_utf8JsonWriterSnippet = _utf8JsonWriterParameter.As<Utf8JsonWriter>();
9597
_mrwOptionsParameterSnippet = _serializationOptionsParameter.As<ModelReaderWriterOptions>();
9698
_jsonElementParameterSnippet = _jsonElementDeserializationParam.As<JsonElement>();
@@ -482,7 +484,7 @@ internal MethodProvider BuildPersistableModelWriteCoreMethod()
482484
? MethodSignatureModifiers.Private
483485
: MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual;
484486

485-
if (_shouldOverrideMethods)
487+
if (_shouldOverrideMethods && !_shouldSkipDerivedSerializationMethodOverrides)
486488
{
487489
modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override;
488490
}
@@ -506,7 +508,7 @@ internal MethodProvider BuildPersistableModelCreateCoreMethod()
506508
? MethodSignatureModifiers.Private
507509
: MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual;
508510

509-
if (_shouldOverrideMethods)
511+
if (_shouldOverrideMethods && !_shouldSkipDerivedSerializationMethodOverrides)
510512
{
511513
modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override;
512514
}
@@ -554,7 +556,7 @@ internal MethodProvider BuildJsonModelCreateCoreMethod()
554556
? MethodSignatureModifiers.Private
555557
: MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual;
556558

557-
if (_shouldOverrideMethods)
559+
if (_shouldOverrideMethods && !_shouldSkipDerivedSerializationMethodOverrides)
558560
{
559561
modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override;
560562
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Linq;
6+
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
7+
using Microsoft.TypeSpec.Generator.Input;
8+
using Microsoft.TypeSpec.Generator.Primitives;
9+
using Microsoft.TypeSpec.Generator.Providers;
10+
using Microsoft.TypeSpec.Generator.Tests.Common;
11+
using NUnit.Framework;
12+
13+
namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.Providers.MrwSerializationTypeDefinitions
14+
{
15+
/// <summary>
16+
/// Tests that serialization methods use correct modifiers when a model's base is
17+
/// <see cref="SystemObjectModelProvider"/>. This validates behavior that would be
18+
/// impossible with <see cref="SystemObjectTypeProvider"/> (which cannot serve as
19+
/// <see cref="ModelProvider.BaseModelProvider"/>).
20+
/// </summary>
21+
internal class SystemObjectModelSerializationTests
22+
{
23+
/// <summary>
24+
/// Creates a derived model with a SystemObjectModelProvider base and returns its serialization.
25+
/// </summary>
26+
private static (ModelProvider Model, MrwSerializationTypeDefinition Serialization) CreateDerivedModelWithSystemBase()
27+
{
28+
var baseProp = InputFactory.Property("Name", InputPrimitiveType.String);
29+
var baseInputModel = InputFactory.Model("Resource", properties: [baseProp]);
30+
var derivedProp = InputFactory.Property("Location", InputPrimitiveType.String);
31+
var derivedInputModel = InputFactory.Model("TrackedResource", properties: [derivedProp], baseModel: baseInputModel);
32+
33+
// Use typeof(object) as a stand-in framework type.
34+
// SystemObjectModelProvider extracts name/namespace from the CSharpType.
35+
var systemType = new CSharpType(typeof(object));
36+
var systemBase = new SystemObjectModelProvider(systemType, baseInputModel);
37+
38+
var generator = MockHelpers.LoadMockGenerator(
39+
inputModels: () => [baseInputModel, derivedInputModel],
40+
createModelCore: (model) =>
41+
{
42+
if (model.Name == "Resource")
43+
return systemBase;
44+
return new ModelProvider(model);
45+
},
46+
createSerializationsCore: (inputType, typeProvider) =>
47+
inputType is InputModelType modelType && typeProvider is ModelProvider mp
48+
? [new MrwSerializationTypeDefinition(modelType, mp)]
49+
: []);
50+
generator.Object.TypeFactory.RootInputModels.Add(derivedInputModel);
51+
generator.Object.TypeFactory.RootOutputModels.Add(derivedInputModel);
52+
53+
var derived = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(derivedInputModel) as ModelProvider;
54+
Assert.IsNotNull(derived);
55+
Assert.IsInstanceOf<SystemObjectModelProvider>(derived!.BaseModelProvider);
56+
57+
var serializations = derived.SerializationProviders;
58+
Assert.AreEqual(1, serializations.Count);
59+
return (derived, (MrwSerializationTypeDefinition)serializations[0]);
60+
}
61+
62+
/// <summary>
63+
/// Creates a derived model with a regular (non-system) ModelProvider base and returns its serialization.
64+
/// </summary>
65+
private static (ModelProvider Model, MrwSerializationTypeDefinition Serialization) CreateDerivedModelWithRegularBase()
66+
{
67+
var baseProp = InputFactory.Property("Name", InputPrimitiveType.String);
68+
var baseInputModel = InputFactory.Model("Resource", properties: [baseProp]);
69+
var derivedProp = InputFactory.Property("Location", InputPrimitiveType.String);
70+
var derivedInputModel = InputFactory.Model("TrackedResource", properties: [derivedProp], baseModel: baseInputModel);
71+
72+
var generator = MockHelpers.LoadMockGenerator(
73+
inputModels: () => [baseInputModel, derivedInputModel],
74+
createSerializationsCore: (inputType, typeProvider) =>
75+
inputType is InputModelType modelType && typeProvider is ModelProvider mp
76+
? [new MrwSerializationTypeDefinition(modelType, mp)]
77+
: []);
78+
generator.Object.TypeFactory.RootInputModels.Add(derivedInputModel);
79+
generator.Object.TypeFactory.RootOutputModels.Add(derivedInputModel);
80+
81+
var derived = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(derivedInputModel) as ModelProvider;
82+
Assert.IsNotNull(derived);
83+
Assert.IsNotInstanceOf<SystemObjectModelProvider>(derived!.BaseModelProvider);
84+
85+
var serializations = derived.SerializationProviders;
86+
Assert.AreEqual(1, serializations.Count);
87+
return (derived, (MrwSerializationTypeDefinition)serializations[0]);
88+
}
89+
90+
// -------------------------------------------------------------------
91+
// JsonModelWriteCore: always 'override' for both system and regular base
92+
// (the framework base type defines JsonModelWriteCore, so we override it)
93+
// -------------------------------------------------------------------
94+
95+
[Test]
96+
public void JsonModelWriteCore_IsOverride_WhenBaseIsSystemObject()
97+
{
98+
var (_, serialization) = CreateDerivedModelWithSystemBase();
99+
var method = serialization.BuildJsonModelWriteCoreMethod();
100+
101+
Assert.IsNotNull(method);
102+
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
103+
"JsonModelWriteCore should be 'override' even with SystemObjectModelProvider base");
104+
Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual),
105+
"JsonModelWriteCore should NOT be 'virtual' when base exists");
106+
}
107+
108+
[Test]
109+
public void JsonModelWriteCore_IsOverride_WhenBaseIsRegularModel()
110+
{
111+
var (_, serialization) = CreateDerivedModelWithRegularBase();
112+
var method = serialization.BuildJsonModelWriteCoreMethod();
113+
114+
Assert.IsNotNull(method);
115+
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
116+
"JsonModelWriteCore should be 'override' with regular base too");
117+
}
118+
119+
// -------------------------------------------------------------------
120+
// PersistableModelWriteCore: 'virtual' with system base, 'override' with regular
121+
// (the framework base already implements this; derived model re-introduces it)
122+
// -------------------------------------------------------------------
123+
124+
[Test]
125+
public void PersistableModelWriteCore_IsVirtual_WhenBaseIsSystemObject()
126+
{
127+
var (_, serialization) = CreateDerivedModelWithSystemBase();
128+
var method = serialization.BuildPersistableModelWriteCoreMethod();
129+
130+
Assert.IsNotNull(method);
131+
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual),
132+
"PersistableModelWriteCore should be 'virtual' when base is SystemObjectModelProvider " +
133+
"(framework already has this method; derived re-introduces, not overrides)");
134+
Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
135+
"PersistableModelWriteCore should NOT be 'override' with SystemObjectModelProvider base");
136+
}
137+
138+
[Test]
139+
public void PersistableModelWriteCore_IsOverride_WhenBaseIsRegularModel()
140+
{
141+
var (_, serialization) = CreateDerivedModelWithRegularBase();
142+
var method = serialization.BuildPersistableModelWriteCoreMethod();
143+
144+
Assert.IsNotNull(method);
145+
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
146+
"PersistableModelWriteCore should be 'override' with regular base model");
147+
}
148+
149+
// -------------------------------------------------------------------
150+
// PersistableModelCreateCore: 'virtual' with system base, 'override' with regular
151+
// -------------------------------------------------------------------
152+
153+
[Test]
154+
public void PersistableModelCreateCore_IsVirtual_WhenBaseIsSystemObject()
155+
{
156+
var (_, serialization) = CreateDerivedModelWithSystemBase();
157+
var method = serialization.BuildPersistableModelCreateCoreMethod();
158+
159+
Assert.IsNotNull(method);
160+
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual),
161+
"PersistableModelCreateCore should be 'virtual' when base is SystemObjectModelProvider");
162+
Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
163+
"PersistableModelCreateCore should NOT be 'override' with SystemObjectModelProvider base");
164+
}
165+
166+
[Test]
167+
public void PersistableModelCreateCore_IsOverride_WhenBaseIsRegularModel()
168+
{
169+
var (_, serialization) = CreateDerivedModelWithRegularBase();
170+
var method = serialization.BuildPersistableModelCreateCoreMethod();
171+
172+
Assert.IsNotNull(method);
173+
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
174+
"PersistableModelCreateCore should be 'override' with regular base model");
175+
}
176+
177+
// -------------------------------------------------------------------
178+
// JsonModelCreateCore: 'virtual' with system base, 'override' with regular
179+
// -------------------------------------------------------------------
180+
181+
[Test]
182+
public void JsonModelCreateCore_IsVirtual_WhenBaseIsSystemObject()
183+
{
184+
var (_, serialization) = CreateDerivedModelWithSystemBase();
185+
var method = serialization.BuildJsonModelCreateCoreMethod();
186+
187+
Assert.IsNotNull(method);
188+
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual),
189+
"JsonModelCreateCore should be 'virtual' when base is SystemObjectModelProvider");
190+
Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
191+
"JsonModelCreateCore should NOT be 'override' with SystemObjectModelProvider base");
192+
}
193+
194+
[Test]
195+
public void JsonModelCreateCore_IsOverride_WhenBaseIsRegularModel()
196+
{
197+
var (_, serialization) = CreateDerivedModelWithRegularBase();
198+
var method = serialization.BuildJsonModelCreateCoreMethod();
199+
200+
Assert.IsNotNull(method);
201+
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
202+
"JsonModelCreateCore should be 'override' with regular base model");
203+
}
204+
}
205+
}

0 commit comments

Comments
 (0)