diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 0cecfe33b29..1ca41296879 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -66,6 +66,7 @@ public partial class MrwSerializationTypeDefinition : TypeProvider private ConstructorProvider? _serializationConstructor; // Flag to determine if the model should override the serialization methods private readonly bool _shouldOverrideMethods; + private readonly bool _hasSystemObjectModelBase; private readonly Lazy _additionalProperties; private CSharpType RootType => _rootType ??= GetRootModelType(); @@ -91,6 +92,7 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m _additionalBinaryDataProperty = new(GetAdditionalBinaryDataPropertiesProp); _additionalProperties = new(() => [.. _model.Properties.Where(p => p.IsAdditionalProperties)]); _shouldOverrideMethods = _model.BaseModelProvider != null && !_isStruct; + _hasSystemObjectModelBase = _model.BaseModelProvider is SystemObjectModelProvider; _utf8JsonWriterSnippet = _utf8JsonWriterParameter.As(); _mrwOptionsParameterSnippet = _serializationOptionsParameter.As(); _jsonElementParameterSnippet = _jsonElementDeserializationParam.As(); @@ -482,7 +484,7 @@ internal MethodProvider BuildPersistableModelWriteCoreMethod() ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (_shouldOverrideMethods) + if (_shouldOverrideMethods && !_hasSystemObjectModelBase) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -506,7 +508,7 @@ internal MethodProvider BuildPersistableModelCreateCoreMethod() ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (_shouldOverrideMethods) + if (_shouldOverrideMethods && !_hasSystemObjectModelBase) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } @@ -554,7 +556,7 @@ internal MethodProvider BuildJsonModelCreateCoreMethod() ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Protected | MethodSignatureModifiers.Virtual; - if (_shouldOverrideMethods) + if (_shouldOverrideMethods && !_hasSystemObjectModelBase) { modifiers = MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SystemObjectModelSerializationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SystemObjectModelSerializationTests.cs new file mode 100644 index 00000000000..94648eebfeb --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/SystemObjectModelSerializationTests.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using Microsoft.TypeSpec.Generator.ClientModel.Providers; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Tests.Common; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.Providers.MrwSerializationTypeDefinitions +{ + /// + /// Tests that serialization methods use correct modifiers when a model's base is + /// . This validates behavior that would be + /// impossible with (which cannot serve as + /// ). + /// + internal class SystemObjectModelSerializationTests + { + /// + /// Creates a derived model with a SystemObjectModelProvider base and returns its serialization. + /// + private static (ModelProvider Model, MrwSerializationTypeDefinition Serialization) CreateDerivedModelWithSystemBase() + { + var baseProp = InputFactory.Property("Name", InputPrimitiveType.String); + var baseInputModel = InputFactory.Model("Resource", properties: [baseProp]); + var derivedProp = InputFactory.Property("Location", InputPrimitiveType.String); + var derivedInputModel = InputFactory.Model("TrackedResource", properties: [derivedProp], baseModel: baseInputModel); + + // Use typeof(object) as a stand-in framework type. + // SystemObjectModelProvider extracts name/namespace from the CSharpType. + var systemType = new CSharpType(typeof(object)); + var systemBase = new SystemObjectModelProvider(systemType, baseInputModel); + + var generator = MockHelpers.LoadMockGenerator( + inputModels: () => [baseInputModel, derivedInputModel], + createModelCore: (model) => + { + if (model.Name == "Resource") + return systemBase; + return new ModelProvider(model); + }, + createSerializationsCore: (inputType, typeProvider) => + inputType is InputModelType modelType && typeProvider is ModelProvider mp + ? [new MrwSerializationTypeDefinition(modelType, mp)] + : []); + generator.Object.TypeFactory.RootInputModels.Add(derivedInputModel); + generator.Object.TypeFactory.RootOutputModels.Add(derivedInputModel); + + var derived = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(derivedInputModel) as ModelProvider; + Assert.IsNotNull(derived); + Assert.IsInstanceOf(derived!.BaseModelProvider); + + var serializations = derived.SerializationProviders; + Assert.AreEqual(1, serializations.Count); + return (derived, (MrwSerializationTypeDefinition)serializations[0]); + } + + /// + /// Creates a derived model with a regular (non-system) ModelProvider base and returns its serialization. + /// + private static (ModelProvider Model, MrwSerializationTypeDefinition Serialization) CreateDerivedModelWithRegularBase() + { + var baseProp = InputFactory.Property("Name", InputPrimitiveType.String); + var baseInputModel = InputFactory.Model("Resource", properties: [baseProp]); + var derivedProp = InputFactory.Property("Location", InputPrimitiveType.String); + var derivedInputModel = InputFactory.Model("TrackedResource", properties: [derivedProp], baseModel: baseInputModel); + + var generator = MockHelpers.LoadMockGenerator( + inputModels: () => [baseInputModel, derivedInputModel], + createSerializationsCore: (inputType, typeProvider) => + inputType is InputModelType modelType && typeProvider is ModelProvider mp + ? [new MrwSerializationTypeDefinition(modelType, mp)] + : []); + generator.Object.TypeFactory.RootInputModels.Add(derivedInputModel); + generator.Object.TypeFactory.RootOutputModels.Add(derivedInputModel); + + var derived = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(derivedInputModel) as ModelProvider; + Assert.IsNotNull(derived); + Assert.IsNotInstanceOf(derived!.BaseModelProvider); + + var serializations = derived.SerializationProviders; + Assert.AreEqual(1, serializations.Count); + return (derived, (MrwSerializationTypeDefinition)serializations[0]); + } + + // ------------------------------------------------------------------- + // JsonModelWriteCore: always 'override' for both system and regular base + // (the framework base type defines JsonModelWriteCore, so we override it) + // ------------------------------------------------------------------- + + [Test] + public void JsonModelWriteCore_IsOverride_WhenBaseIsSystemObject() + { + var (_, serialization) = CreateDerivedModelWithSystemBase(); + var method = serialization.BuildJsonModelWriteCoreMethod(); + + Assert.IsNotNull(method); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override), + "JsonModelWriteCore should be 'override' even with SystemObjectModelProvider base"); + Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual), + "JsonModelWriteCore should NOT be 'virtual' when base exists"); + } + + [Test] + public void JsonModelWriteCore_IsOverride_WhenBaseIsRegularModel() + { + var (_, serialization) = CreateDerivedModelWithRegularBase(); + var method = serialization.BuildJsonModelWriteCoreMethod(); + + Assert.IsNotNull(method); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override), + "JsonModelWriteCore should be 'override' with regular base too"); + } + + // ------------------------------------------------------------------- + // PersistableModelWriteCore: 'virtual' with system base, 'override' with regular + // (the framework base already implements this; derived model re-introduces it) + // ------------------------------------------------------------------- + + [Test] + public void PersistableModelWriteCore_IsVirtual_WhenBaseIsSystemObject() + { + var (_, serialization) = CreateDerivedModelWithSystemBase(); + var method = serialization.BuildPersistableModelWriteCoreMethod(); + + Assert.IsNotNull(method); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual), + "PersistableModelWriteCore should be 'virtual' when base is SystemObjectModelProvider " + + "(framework already has this method; derived re-introduces, not overrides)"); + Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override), + "PersistableModelWriteCore should NOT be 'override' with SystemObjectModelProvider base"); + } + + [Test] + public void PersistableModelWriteCore_IsOverride_WhenBaseIsRegularModel() + { + var (_, serialization) = CreateDerivedModelWithRegularBase(); + var method = serialization.BuildPersistableModelWriteCoreMethod(); + + Assert.IsNotNull(method); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override), + "PersistableModelWriteCore should be 'override' with regular base model"); + } + + // ------------------------------------------------------------------- + // PersistableModelCreateCore: 'virtual' with system base, 'override' with regular + // ------------------------------------------------------------------- + + [Test] + public void PersistableModelCreateCore_IsVirtual_WhenBaseIsSystemObject() + { + var (_, serialization) = CreateDerivedModelWithSystemBase(); + var method = serialization.BuildPersistableModelCreateCoreMethod(); + + Assert.IsNotNull(method); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual), + "PersistableModelCreateCore should be 'virtual' when base is SystemObjectModelProvider"); + Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override), + "PersistableModelCreateCore should NOT be 'override' with SystemObjectModelProvider base"); + } + + [Test] + public void PersistableModelCreateCore_IsOverride_WhenBaseIsRegularModel() + { + var (_, serialization) = CreateDerivedModelWithRegularBase(); + var method = serialization.BuildPersistableModelCreateCoreMethod(); + + Assert.IsNotNull(method); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override), + "PersistableModelCreateCore should be 'override' with regular base model"); + } + + // ------------------------------------------------------------------- + // JsonModelCreateCore: 'virtual' with system base, 'override' with regular + // ------------------------------------------------------------------- + + [Test] + public void JsonModelCreateCore_IsVirtual_WhenBaseIsSystemObject() + { + var (_, serialization) = CreateDerivedModelWithSystemBase(); + var method = serialization.BuildJsonModelCreateCoreMethod(); + + Assert.IsNotNull(method); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual), + "JsonModelCreateCore should be 'virtual' when base is SystemObjectModelProvider"); + Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override), + "JsonModelCreateCore should NOT be 'override' with SystemObjectModelProvider base"); + } + + [Test] + public void JsonModelCreateCore_IsOverride_WhenBaseIsRegularModel() + { + var (_, serialization) = CreateDerivedModelWithRegularBase(); + var method = serialization.BuildJsonModelCreateCoreMethod(); + + Assert.IsNotNull(method); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override), + "JsonModelCreateCore should be 'override' with regular base model"); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 4682a736fb4..6a62ba76856 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -169,6 +169,48 @@ private IReadOnlyList BuildDerivedModels() public ModelProvider? BaseModelProvider => _baseModelProvider ??= BuildBaseModelProvider(); + + /// + /// Updates the model provider, optionally replacing the base model provider. + /// When is specified, the model is automatically + /// reset to rebuild all dependent members (constructors, properties, etc.). + /// + /// The new base model provider to replace the current one. + public void Update( + ModelProvider? baseModelProvider = null, + IEnumerable? methods = null, + IEnumerable? constructors = null, + IEnumerable? properties = null, + IEnumerable? fields = null, + IEnumerable? serializations = null, + IEnumerable? nestedTypes = null, + IEnumerable? attributes = default, + IEnumerable? implements = null, + XmlDocProvider? xmlDocs = null, + TypeSignatureModifiers? modifiers = null, + string? name = null, + string? @namespace = null, + string? relativeFilePath = null, + bool reset = false) + { + if (baseModelProvider != null) + { + _baseModelProvider = baseModelProvider; + reset = true; + } + base.Update(methods, constructors, properties, fields, serializations, nestedTypes, attributes, implements, xmlDocs, modifiers, name, @namespace, relativeFilePath, reset); + } + + /// + public override void Reset() + { + base.Reset(); + _rawDataField = null; + _additionalPropertyFields = null; + _additionalPropertyProperties = null; + _fullConstructor = null; + } + protected FieldProvider? RawDataField => _rawDataField ??= BuildRawDataField(); private List AdditionalPropertyFields => _additionalPropertyFields ??= BuildAdditionalPropertyFields(); private List AdditionalPropertyProperties => _additionalPropertyProperties ??= BuildAdditionalPropertyProperties(); @@ -315,10 +357,23 @@ private static bool IsDiscriminator(InputProperty property) return customBaseModel; } - // If the custom base type has a namespace (external type), we don't return it here - // as it's handled by BuildBaseTypeProvider() which returns a TypeProvider + // If the custom base type has a namespace (external type), try name+namespace based + // lookup as a fallback. This handles the case where CSharpType equality fails due to + // framework vs non-framework type mismatch (e.g., a CSharpType from Roslyn with + // _type=typeof(T) vs a CSharpType from a model provider with _type=null). if (!string.IsNullOrEmpty(baseType?.Namespace)) { + foreach (var (mapKey, mapValue) in CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap) + { + if (mapValue is ModelProvider mp && + mapKey.Name == baseType.Name && + mapKey.Namespace == baseType.Namespace) + { + // Cache with the custom code's CSharpType for future lookups + CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = mp; + return mp; + } + } return null; } } @@ -485,6 +540,22 @@ protected internal override PropertyProvider[] BuildProperties() var propertiesCount = _inputModel.Properties.Count; var properties = new List(propertiesCount + 1); Dictionary baseProperties = EnumerateBaseModels().SelectMany(m => m.Properties).GroupBy(x => x.Name).Select(g => g.First()).ToDictionary(p => p.Name) ?? []; + // When the base model provider is a SystemObjectModelProvider (e.g., from a custom code base type + // override), also include properties from its InputModel chain. This handles the case where the + // spec-defined base differs from the custom code base (e.g., spec says Resource but custom code + // says TrackedResourceData), ensuring all base properties are properly deduplicated. + if (HasSystemObjectModelBase() && BaseModelProvider is SystemObjectModelProvider systemObjBase) + { + var baseInputModel = systemObjBase._inputModel; + while (baseInputModel != null) + { + foreach (var prop in baseInputModel.Properties) + { + baseProperties.TryAdd(prop.Name, prop); + } + baseInputModel = baseInputModel.BaseModel; + } + } // Build a set of serialized names for base discriminator properties to handle cases where // the derived model has a discriminator with a different C# name but the same wire name HashSet baseDiscriminatorSerializedNames = EnumerateBaseModels() @@ -542,6 +613,13 @@ protected internal override PropertyProvider[] BuildProperties() var baseProperty = baseProperties.GetValueOrDefault(property.Name); if (baseProperty is not null) { + // If the base chain includes a SystemObjectModelProvider, the framework type + // already defines this property — skip generating it in the derived model. + if (HasSystemObjectModelBase()) + { + continue; + } + if (DomainEqual(baseProperty, property)) { outputProperty.Modifiers |= MethodSignatureModifiers.Override; @@ -579,6 +657,23 @@ private IEnumerable EnumerateBaseModels() } } + /// + /// Checks if any ancestor in the base model chain is a . + /// + private bool HasSystemObjectModelBase() + { + var model = BaseModelProvider; + while (model != null) + { + if (model is SystemObjectModelProvider) + { + return true; + } + model = model.BaseModelProvider; + } + return false; + } + private static bool DomainEqual(InputProperty baseProperty, InputProperty derivedProperty) { if (baseProperty.Type.Name != derivedProperty.Type.Name) @@ -1198,7 +1293,7 @@ private ValueExpression GetConversion(PropertyProvider? property = default, Fiel /// Builds the raw data field for the model to be used for serialization. /// /// The constructed if the model should generate the field. - private FieldProvider? BuildRawDataField() + protected virtual FieldProvider? BuildRawDataField() { // check if there is a raw data field on any of the base models, if so, we do not have to have one here. var baseModelProvider = BaseModelProvider; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs new file mode 100644 index 00000000000..83b7098a8d5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectModelProvider.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; + +namespace Microsoft.TypeSpec.Generator.Providers +{ + /// + /// Represents a model type from an external assembly (system or referenced assembly) that is mapped + /// from an input model type. Unlike which extends , + /// this class extends so it can serve as a + /// for derived models that inherit from system types. + /// + /// This is used when a code generator maps an input model (e.g., an ARM Resource type) to an existing + /// framework type (e.g., ResourceData) rather than generating a new type. + /// + /// + public class SystemObjectModelProvider : ModelProvider + { + private readonly CSharpType _systemType; + + /// + /// Initializes a new instance of . + /// + /// The CSharp type from the external/system assembly. + /// The input model type that this system type replaces. + public SystemObjectModelProvider(CSharpType systemType, InputModelType inputModel) : base(inputModel) + { + _systemType = systemType ?? throw new ArgumentNullException(nameof(systemType)); + CrossLanguageDefinitionId = inputModel.CrossLanguageDefinitionId; + } + + /// + /// Gets the underlying system that this provider wraps. + /// + public CSharpType SystemType => _systemType; + + /// + /// Gets the cross-language definition ID from the input model. + /// + public string CrossLanguageDefinitionId { get; } + + /// + // _systemType may be null when called from base constructor before field assignment. + protected override string BuildName() => _systemType?.Name ?? string.Empty; + + /// + protected override string BuildRelativeFilePath() + => throw new InvalidOperationException("This type should not be writing in generation"); + + /// + // _systemType may be null when called from base constructor before field assignment. + protected override string BuildNamespace() => _systemType?.Namespace ?? string.Empty; + + /// + /// Framework types manage their own fields; no generated fields needed. + /// + protected internal override FieldProvider[] BuildFields() => []; + + /// + /// Framework types have their own serialization; no generated serialization providers needed. + /// + protected override TypeProvider[] BuildSerializationProviders() => []; + + /// + /// Framework types manage their own raw data field. + /// Returns null so derived models create their own. + /// + protected override FieldProvider? BuildRawDataField() => null; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs new file mode 100644 index 00000000000..d0e79f9ce9b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/SystemObjectModelProviderTests.cs @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Tests.Common; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.Tests.Providers +{ + /// + /// Tests for that demonstrate capabilities + /// missing from the existing . + /// + /// extends and cannot + /// serve as a . + /// extends to fill this gap — enabling derived models to inherit + /// from framework/system types while getting proper property deduplication, raw data field, and + /// serialization handling. + /// + /// + public class SystemObjectModelProviderTests + { + /// + /// Creates a non-framework CSharpType with the given name and namespace. + /// Uses the internal constructor accessible via InternalsVisibleTo. + /// + private static CSharpType CreateSystemCSharpType(string name, string ns) + => new(name, ns, isValueType: false, isNullable: false, declaringType: null, + args: Array.Empty(), isPublic: true, isStruct: false); + + [SetUp] + public void Setup() + { + MockHelpers.LoadMockGenerator(); + } + + // ------------------------------------------------------------------- + // 1. Type hierarchy: ModelProvider vs TypeProvider + // ------------------------------------------------------------------- + + [Test] + public void SystemObjectModelProvider_IsModelProvider() + { + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var inputModel = InputFactory.Model("Resource", properties: []); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + Assert.IsInstanceOf(provider); + } + + [Test] + public void SystemObjectTypeProvider_IsNotModelProvider() + { + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var provider = new SystemObjectTypeProvider(systemType); + + Assert.IsNotInstanceOf(provider); + Assert.IsInstanceOf(provider); + } + + // ------------------------------------------------------------------- + // 2. Can serve as BaseModelProvider for derived models + // (SystemObjectTypeProvider cannot because it's not a ModelProvider) + // ------------------------------------------------------------------- + + [Test] + public void CanServeAsBaseModelProvider() + { + var baseProp = InputFactory.Property("Name", InputPrimitiveType.String); + var baseInputModel = InputFactory.Model("Resource", properties: [baseProp]); + + var derivedProp = InputFactory.Property("Location", InputPrimitiveType.String); + var derivedInputModel = InputFactory.Model("DerivedResource", properties: [derivedProp], baseModel: baseInputModel); + + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + MockHelpers.LoadMockGenerator( + inputModelTypes: [baseInputModel, derivedInputModel], + createModelCore: (model) => + { + if (model.Name == "Resource") + return new SystemObjectModelProvider(systemType, model); + return new ModelProvider(model); + }); + + var derivedProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(derivedInputModel) as ModelProvider; + Assert.IsNotNull(derivedProvider); + + // The base should be a SystemObjectModelProvider — impossible with SystemObjectTypeProvider + Assert.IsNotNull(derivedProvider!.BaseModelProvider); + Assert.IsInstanceOf(derivedProvider.BaseModelProvider); + } + + // ------------------------------------------------------------------- + // 3. Property deduplication: properties matching framework base are skipped + // ------------------------------------------------------------------- + + [Test] + public void DerivedModel_SkipsPropertiesDefinedInSystemObjectBase() + { + var baseProp = InputFactory.Property("Name", InputPrimitiveType.String); + var baseInputModel = InputFactory.Model("Resource", properties: [baseProp]); + + // Derived re-declares "Name" (same as base) + has its own "Location" + var derivedNameProp = InputFactory.Property("Name", InputPrimitiveType.String); + var derivedLocationProp = InputFactory.Property("Location", InputPrimitiveType.String); + var derivedInputModel = InputFactory.Model( + "TrackedResource", + properties: [derivedNameProp, derivedLocationProp], + baseModel: baseInputModel); + + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + MockHelpers.LoadMockGenerator( + inputModelTypes: [baseInputModel, derivedInputModel], + createModelCore: (model) => + { + if (model.Name == "Resource") + return new SystemObjectModelProvider(systemType, model); + return new ModelProvider(model); + }); + + var derived = CodeModelGenerator.Instance.TypeFactory.CreateModel(derivedInputModel) as ModelProvider; + Assert.IsNotNull(derived); + + // "Name" should be skipped (defined in the framework base) + // Only "Location" should be generated + var propertyNames = derived!.Properties.Select(p => p.Name).ToList(); + Assert.IsFalse(propertyNames.Contains("Name"), + "Property 'Name' should be skipped because it is defined in the SystemObjectModelProvider base"); + Assert.IsTrue(propertyNames.Contains("Location"), + "Property 'Location' should be generated because it is NOT in the base"); + } + + [Test] + public void RegularBaseModel_DoesNotSkipMatchingProperties() + { + // Same setup but with a regular ModelProvider base (not SystemObjectModelProvider) + var baseProp = InputFactory.Property("Name", InputPrimitiveType.String); + var baseInputModel = InputFactory.Model("Resource", properties: [baseProp]); + + var derivedNameProp = InputFactory.Property("Name", InputPrimitiveType.String); + var derivedLocationProp = InputFactory.Property("Location", InputPrimitiveType.String); + var derivedInputModel = InputFactory.Model( + "TrackedResource", + properties: [derivedNameProp, derivedLocationProp], + baseModel: baseInputModel); + + MockHelpers.LoadMockGenerator(inputModelTypes: [baseInputModel, derivedInputModel]); + + var derived = new ModelProvider(derivedInputModel); + + // With a regular base, both properties should be generated (Name as override) + var propertyNames = derived.Properties.Select(p => p.Name).ToList(); + Assert.IsTrue(propertyNames.Contains("Name"), + "Property 'Name' should be generated with override modifier for regular inheritance"); + Assert.IsTrue(propertyNames.Contains("Location")); + } + + // ------------------------------------------------------------------- + // 4. Raw data field: SystemObjectModelProvider returns null, + // so derived models create their own field + // ------------------------------------------------------------------- + + [Test] + public void SystemObjectModelProvider_HasNoRawDataField_InFields() + { + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var inputModel = InputFactory.Model("Resource", properties: []); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + // SystemObjectModelProvider should have no fields at all (including no raw data field) + Assert.IsEmpty(provider.Fields, + "SystemObjectModelProvider should return no fields — the framework type manages its own raw data"); + } + + [Test] + public void DerivedModel_CreatesOwnRawDataField_WhenBaseIsSystemObject() + { + var baseProp = InputFactory.Property("Name", InputPrimitiveType.String); + var baseInputModel = InputFactory.Model("Resource", properties: [baseProp]); + var derivedProp = InputFactory.Property("Location", InputPrimitiveType.String); + var derivedInputModel = InputFactory.Model("TrackedResource", properties: [derivedProp], baseModel: baseInputModel); + + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + MockHelpers.LoadMockGenerator( + inputModelTypes: [baseInputModel, derivedInputModel], + createModelCore: (model) => + { + if (model.Name == "Resource") + return new SystemObjectModelProvider(systemType, model); + return new ModelProvider(model); + }); + + var derived = CodeModelGenerator.Instance.TypeFactory.CreateModel(derivedInputModel) as ModelProvider; + Assert.IsNotNull(derived); + + // Derived model should have its own raw data field since the system base has none + var rawDataField = derived!.Fields.FirstOrDefault(f => f.Name == "_additionalBinaryDataProperties"); + Assert.IsNotNull(rawDataField, + "Derived model should create its own raw data field when SystemObjectModelProvider base has none"); + } + + // ------------------------------------------------------------------- + // 5. Empty members: SystemObjectModelProvider generates nothing + // (framework type provides everything at runtime) + // ------------------------------------------------------------------- + + [Test] + public void SystemObjectModelProvider_Properties_ComeFromInputModel() + { + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var prop = InputFactory.Property("Name", InputPrimitiveType.String); + var inputModel = InputFactory.Model("Resource", properties: [prop]); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + // Properties are now built from the input model so derived models can see + // base properties for constructor building and property deduplication. + var propertyNames = provider.Properties.Select(p => p.Name).ToList(); + Assert.IsTrue(propertyNames.Contains("Name"), + "SystemObjectModelProvider should expose properties from the input model for derived model constructor building"); + } + + [Test] + public void SystemObjectModelProvider_Fields_AreEmpty() + { + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var inputModel = InputFactory.Model("Resource", properties: []); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + Assert.IsEmpty(provider.Fields, + "SystemObjectModelProvider should not generate fields"); + } + + [Test] + public void SystemObjectModelProvider_Constructors_AreBuiltFromInputModel() + { + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var prop = InputFactory.Property("Name", InputPrimitiveType.String, isRequired: true); + var inputModel = InputFactory.Model("Resource", properties: [prop]); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + // Constructors are now built from the input model so derived models can use + // BaseModelProvider.FullConstructor.Signature.Parameters for constructor building. + Assert.IsNotEmpty(provider.Constructors, + "SystemObjectModelProvider should build constructors from the input model for derived model constructor building"); + } + + [Test] + public void SystemObjectModelProvider_SerializationProviders_AreEmpty() + { + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var inputModel = InputFactory.Model("Resource", properties: []); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + Assert.IsEmpty(provider.SerializationProviders, + "SystemObjectModelProvider should not generate serialization providers"); + } + + // ------------------------------------------------------------------- + // 6. Name and namespace come from the system CSharpType + // ------------------------------------------------------------------- + + [Test] + public void Name_ComesFromSystemType_ViaSystemTypeProperty() + { + var systemType = CreateSystemCSharpType("TrackedResourceData", "Azure.ResourceManager.Models"); + var inputModel = InputFactory.Model("TrackedResource", properties: [], access: "internal"); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + // The SystemType property always reflects the original system type + Assert.AreEqual("TrackedResourceData", provider.SystemType.Name); + } + + [Test] + public void Namespace_ComesFromSystemType_ViaSystemTypeProperty() + { + var systemType = CreateSystemCSharpType("TrackedResourceData", "Azure.ResourceManager.Models"); + var inputModel = InputFactory.Model("TrackedResource", properties: [], access: "internal"); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + Assert.AreEqual("Azure.ResourceManager.Models", provider.SystemType.Namespace); + } + + [Test] + public void Name_ComesFromSystemType_WhenTypeNotEarlyCached() + { + // When access is not "public", the ModelProvider constructor doesn't call AddTypeToKeep, + // so Type is not eagerly evaluated and BuildName() is deferred until after _systemType is set. + var systemType = CreateSystemCSharpType("TrackedResourceData", "Azure.ResourceManager.Models"); + var inputModel = InputFactory.Model("TrackedResource", properties: [], access: "internal"); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + Assert.AreEqual("TrackedResourceData", provider.Name); + Assert.AreEqual("Azure.ResourceManager.Models", provider.Type.Namespace); + } + + [Test] + public void CrossLanguageDefinitionId_ComesFromInputModel() + { + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var inputModel = InputFactory.Model("Resource", properties: []); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + Assert.AreEqual(inputModel.CrossLanguageDefinitionId, provider.CrossLanguageDefinitionId); + } + + // ------------------------------------------------------------------- + // 7. BuildRelativeFilePath throws — system types should not be written + // ------------------------------------------------------------------- + + [Test] + public void BuildRelativeFilePath_Throws() + { + var systemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var inputModel = InputFactory.Model("Resource", properties: []); + var provider = new SystemObjectModelProvider(systemType, inputModel); + + Assert.Throws(() => _ = provider.RelativeFilePath); + } + + // ------------------------------------------------------------------- + // 8. Constructor validation + // ------------------------------------------------------------------- + + [Test] + public void Constructor_ThrowsOnNullSystemType() + { + var inputModel = InputFactory.Model("Resource", properties: []); + Assert.Throws(() => new SystemObjectModelProvider(null!, inputModel)); + } + } +}