diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 79b77b510c9..123253b91ff 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -1183,7 +1183,7 @@ public DuplicateDerivedProvider(InputModelType inputModelType, ModelProvider bas _baseProvider = baseProvider; } - protected override ModelProvider? BuildBaseModelProvider() => _baseProvider; + internal override TypeProvider? BaseTypeProvider => _baseProvider; protected override string BuildName() => "DerivedModel"; } @@ -1196,7 +1196,7 @@ public CyclicBaseProvider(InputModelType inputModelType, bool includeFrameworkPr public ModelProvider? BaseProvider { get; set; } - protected override ModelProvider? BuildBaseModelProvider() => BaseProvider; + internal override TypeProvider? BaseTypeProvider => BaseProvider; } public class FrameworkTypeImplementingOtherFrameworkType : ComplexFrameworkType diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs index 2c54d7968ba..56e937b4885 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/ModelCustomizationTests.cs @@ -320,7 +320,10 @@ public async Task CanCustomizeBaseType(bool declareBaseTypeInCustomCode, bool ca inputModels: () => [inputModel, baseModel], compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(declareBaseTypeInCustomCode.ToString())); - var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + var modelProvider = (ModelProvider)mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + Assert.IsNotNull(modelProvider.BaseTypeProvider); + Assert.AreSame(modelProvider.BaseModelProvider, modelProvider.BaseTypeProvider); // should not have the additionalProperties dictionary Assert.AreEqual(0, modelProvider.Fields.Count); 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 index fcc90582416..7b875f323e2 100644 --- 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 @@ -16,9 +16,7 @@ namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.Providers.MrwSerializat { /// /// 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 { @@ -358,7 +356,7 @@ private class DelayedBaseModelProvider(InputModelType inputModel) : ModelProvide { public ModelProvider? BaseModel { get; set; } - protected override ModelProvider? BuildBaseModelProvider() => BaseModel; + internal override TypeProvider? BaseTypeProvider => BaseModel; protected override CSharpType? BuildBaseType() => BaseModel?.Type; } 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 9f28c6bc482..1d0abe379a8 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 @@ -62,7 +62,6 @@ protected override FormattableString BuildDescription() private bool _buildingRawDataField; private List? _additionalPropertyFields; private List? _additionalPropertyProperties; - private ModelProvider? _baseModelProvider; private ConstructorProvider? _fullConstructor; internal PropertyProvider? DiscriminatorProperty { get; private set; } private ValueExpression DiscriminatorLiteral => Literal(_inputModel.DiscriminatorValue ?? ""); @@ -124,57 +123,77 @@ private IReadOnlyList BuildDerivedModels() return [.. derivedModels]; } - internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= BuildBaseTypeProvider(); + internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= ResolveBaseTypeProvider(); private TypeProvider? _baseTypeProvider; - private TypeProvider? BuildBaseTypeProvider() + private TypeProvider? ResolveBaseTypeProvider() { - // First check if there's a generated base model - if (BaseModelProvider != null) + var baseType = BaseType; + if (baseType is null) + { + return null; + } + + var hasExistingProvider = CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(baseType, out var existingProvider); + if (existingProvider is ModelProvider) { - return BaseModelProvider; + return existingProvider; } - // If there's a custom base type that's not a generated model, create a provider for it - if (CustomCodeView?.BaseType != null && !string.IsNullOrEmpty(CustomCodeView.BaseType.Namespace)) + if (CustomCodeView?.BaseType is null) { - var baseType = CustomCodeView.BaseType; + return existingProvider; + } - // Try to find it in the CSharpTypeMap first - if (CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(baseType, out var existingProvider)) + // Prefer a mapped model over a symbol provider for a custom base that names a generated model. + var matchingModel = CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap + .FirstOrDefault(entry => + entry.Value is ModelProvider && + entry.Key.Name == baseType.Name && + entry.Key.Namespace == baseType.Namespace) + .Value; + if (matchingModel is null) + { + // Ensure visitor-renamed input models have been registered before falling back to a symbol provider. + foreach (var model in CodeModelGenerator.Instance.InputLibrary.InputNamespace.Models) { - return existingProvider; + CodeModelGenerator.Instance.TypeFactory.CreateModel(model); } - // Try to find the type in the customization compilation. Referenced assemblies are - // included so custom bases from framework or external packages are represented by - // normal symbol-backed providers. - var baseTypeProvider = CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( - baseType.Namespace, - baseType.Name, - baseType.DeclaringType?.Name, - includeReferencedAssemblies: true); + matchingModel = CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap + .FirstOrDefault(entry => + entry.Value is ModelProvider && + entry.Key.Name == baseType.Name && + entry.Key.Namespace == baseType.Namespace) + .Value; + } + + if (matchingModel is not null) + { + CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = matchingModel; + return matchingModel; + } - if (baseTypeProvider != null) - { - // Cache it in CSharpTypeMap for future lookups - CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = baseTypeProvider; - return baseTypeProvider; - } + if (hasExistingProvider || string.IsNullOrEmpty(baseType.Namespace)) + { + return existingProvider; + } + + var matchingProvider = CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( + baseType.Namespace, + baseType.Name, + baseType.DeclaringType?.Name, + includeReferencedAssemblies: true); - // If we couldn't find the type symbol, create a SystemObjectTypeProvider that - // represents the external type without member metadata. - var systemObjectTypeProvider = new SystemObjectTypeProvider(baseType); - // Cache it in CSharpTypeMap for future lookups - CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = systemObjectTypeProvider; - return systemObjectTypeProvider; + if (matchingProvider is not null) + { + CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = matchingProvider; } - return null; + return matchingProvider; } - public ModelProvider? BaseModelProvider - => _baseModelProvider ??= BuildBaseModelProvider(); + public ModelProvider? BaseModelProvider => BaseTypeProvider as ModelProvider; /// public override void Reset() @@ -384,37 +403,6 @@ private static bool IsDiscriminator(InputProperty property) return property is InputModelProperty modelProperty && modelProperty.IsDiscriminator; } - protected virtual ModelProvider? BuildBaseModelProvider() - { - var baseType = BaseType; - if (baseType is null) - { - return null; - } - - if (CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(baseType, out var provider) - && provider is ModelProvider modelProvider) - { - return modelProvider; - } - - if (CustomCodeView?.BaseType != null && !string.IsNullOrEmpty(baseType.Namespace)) - { - foreach (var (mapKey, mapValue) in CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap) - { - if (mapValue is ModelProvider model - && mapKey.Name == baseType.Name - && mapKey.Namespace == baseType.Namespace) - { - CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = model; - return model; - } - } - } - - return null; - } - private List BuildAdditionalPropertyFields() { var fields = new List(); 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 index 8708932e11e..327f4f0496b 100644 --- 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 @@ -9,8 +9,7 @@ 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 + /// from an input model type. This class 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 diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectTypeProvider.cs deleted file mode 100644 index 67d4d0c3f34..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectTypeProvider.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using Microsoft.TypeSpec.Generator.Primitives; -using Microsoft.TypeSpec.Generator.Statements; - -namespace Microsoft.TypeSpec.Generator.Providers -{ - /// - /// Represents a type from an external assembly (system or referenced assembly) that is not part of the current generation. - /// This provider is used when a generated model inherits from a type that exists in a referenced assembly - /// but doesn't have a Roslyn type symbol available in the customization compilation. - /// - internal sealed class SystemObjectTypeProvider : TypeProvider - { - private readonly CSharpType _type; - - public SystemObjectTypeProvider(CSharpType type) - { - _type = type ?? throw new ArgumentNullException(nameof(type)); - - if (string.IsNullOrEmpty(_type.Namespace)) - { - throw new ArgumentException("Type must have a namespace", nameof(type)); - } - } - - private protected sealed override TypeProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null; - private protected sealed override TypeProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null; - - protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writing in generation"); - - protected override string BuildName() => _type.Name; - - protected override string BuildNamespace() => _type.Namespace; - - protected override IReadOnlyList BuildAttributes() => []; - - protected override CSharpType? BuildBaseType() => _type.BaseType; - - protected override TypeSignatureModifiers BuildDeclarationModifiers() - { - // Default to public class since we don't have symbol information - return TypeSignatureModifiers.Public | TypeSignatureModifiers.Class; - } - - protected internal override FieldProvider[] BuildFields() => []; - - protected internal override PropertyProvider[] BuildProperties() => []; - - protected internal override ConstructorProvider[] BuildConstructors() => []; - - protected internal override MethodProvider[] BuildMethods() => []; - - protected override bool GetIsEnum() => false; - - protected override CSharpType BuildEnumUnderlyingType() => throw new InvalidOperationException("This type is not an enum"); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs index 07065943b7b..d6c021422f7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/TypeFactory.cs @@ -180,7 +180,7 @@ protected internal TypeFactory() } // Add sentinel before construction to prevent re-entrant creation of the same model - // (e.g., when BuildBaseModelProvider triggers CreateModel for all input models). + // (e.g., when base type resolution triggers CreateModel for all input models). InputTypeToModelProvider[model] = null; // A model marked as external maps to a type that already exists in a framework or diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 9f9945a2360..43a45fb2efa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -503,6 +503,7 @@ public void OverridingBuildBaseType_AutoResolvesBaseModelProviderToNullForNonMod Assert.IsNotNull(actualDerived); Assert.AreEqual(nonModelTypeProvider.Type, actualDerived!.BaseType); + Assert.AreSame(nonModelTypeProvider, actualDerived.BaseTypeProvider); Assert.IsNull(actualDerived.BaseModelProvider); } @@ -565,7 +566,7 @@ public CyclicBaseModelProvider(InputModelType inputModel) : base(inputModel) public ModelProvider? CyclicBase { get; set; } - protected override ModelProvider? BuildBaseModelProvider() => CyclicBase; + internal override TypeProvider? BaseTypeProvider => CyclicBase; } [Test] 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 index e5179daaa47..2eed4b7890e 100644 --- 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 @@ -13,15 +13,9 @@ 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 + /// Tests for , which enables derived models to inherit /// from framework/system types while getting proper property deduplication, raw data field, and /// serialization handling. - /// /// public class SystemObjectModelProviderTests { @@ -53,19 +47,8 @@ public void SystemObjectModelProvider_IsModelProvider() 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] @@ -93,7 +76,6 @@ public void CanServeAsBaseModelProvider() 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); } @@ -385,8 +367,7 @@ public void Constructor_ThrowsOnNullSystemType() // ------------------------------------------------------------------- // 9. A derived discriminated model forwards its discriminator value to a - // SystemObjectModelProvider base constructor. This is impossible with - // SystemObjectTypeProvider because it cannot serve as a BaseModelProvider. + // SystemObjectModelProvider base constructor. // ------------------------------------------------------------------- [Test]