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 37f85d828bf..bfc44cf97dc 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 @@ -118,7 +118,58 @@ private IReadOnlyList BuildDerivedModels() return [.. derivedModels]; } - internal override TypeProvider? BaseTypeProvider => BaseModelProvider; + internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= BuildBaseTypeProvider(); + private TypeProvider? _baseTypeProvider; + + private TypeProvider? BuildBaseTypeProvider() + { + // First check if there's a generated base model + if (BaseModelProvider != null) + { + return BaseModelProvider; + } + + // 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)) + { + var baseType = CustomCodeView.BaseType; + + // Try to find it in the CSharpTypeMap first + if (CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(baseType, out var existingProvider)) + { + return existingProvider; + } + + // If not found, try to look it up from Roslyn's customization compilation + var customization = CodeModelGenerator.Instance.SourceInputModel.Customization; + if (customization != null) + { + // Construct the fully qualified metadata name in .NET format + // For generic types, this uses backtick notation (e.g., "System.Collections.Generic.List`1") + var fullyQualifiedName = baseType.IsGenericType + ? $"{baseType.Namespace}.{baseType.Name}`{baseType.Arguments.Count}" + : $"{baseType.Namespace}.{baseType.Name}"; + + var baseTypeSymbol = customization.GetTypeByMetadataName(fullyQualifiedName); + if (baseTypeSymbol != null) + { + var baseTypeProvider = new NamedTypeSymbolProvider(baseTypeSymbol, customization); + // Cache it in CSharpTypeMap for future lookups + CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = baseTypeProvider; + return baseTypeProvider; + } + } + + // If we couldn't find the type symbol (e.g., type is from a referenced assembly not in customization), + // create a SystemObjectTypeProvider that represents the external type + var systemObjectTypeProvider = new SystemObjectTypeProvider(baseType); + // Cache it in CSharpTypeMap for future lookups + CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = systemObjectTypeProvider; + return systemObjectTypeProvider; + } + + return null; + } public ModelProvider? BaseModelProvider => _baseModelProvider ??= BuildBaseModelProvider(); @@ -241,7 +292,7 @@ private static bool IsDiscriminator(InputProperty property) private ModelProvider? BuildBaseModelProvider() { - // consider models that have been customized to inherit from a different model + // consider models that have been customized to inherit from a different generated model if (CustomCodeView?.BaseType != null) { var baseType = CustomCodeView.BaseType; @@ -256,6 +307,8 @@ private static bool IsDiscriminator(InputProperty property) baseType = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(baseInputModel); } } + + // Try to find the base type in the CSharpTypeMap if (baseType != null && CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue( baseType, out var customBaseType) && @@ -263,6 +316,13 @@ 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 (!string.IsNullOrEmpty(baseType?.Namespace)) + { + return null; + } } if (_inputModel.BaseModel == null) 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 new file mode 100644 index 00000000000..67d4d0c3f34 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/SystemObjectTypeProvider.cs @@ -0,0 +1,61 @@ +// 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/test/Providers/ModelProviders/ModelCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelCustomizationTests.cs index ae09d2e9a53..8cdd6dfd1b2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelCustomizationTests.cs @@ -1507,6 +1507,79 @@ private class TestNamespaceVisitor : NameSpaceVisitor } } + [Test] + public async Task CanCustomizeBaseModelToExternalType() + { + // This test verifies that a model can be customized to inherit from an external base type + // that is not generated during the current generation run (e.g., from another assembly) + var childModel = InputFactory.Model( + "mockInputModel", + properties: [InputFactory.Property("childProp", InputPrimitiveType.String)], + usage: InputModelTypeUsage.Json); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [childModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel") as ModelProvider; + + // Should have customized base type from external assembly + Assert.IsNotNull(modelProvider); + Assert.IsNotNull(modelProvider!.BaseType); + Assert.IsNotNull(modelProvider.BaseTypeProvider); + Assert.AreEqual("ExternalBaseModel", modelProvider.BaseType!.Name); + Assert.AreEqual("Sample.Models", modelProvider.BaseType!.Namespace); + + // The BaseModelProvider should be null since the base is not a generated model + Assert.IsNull(modelProvider.BaseModelProvider); + + // BaseTypeProvider should be NamedTypeSymbolProvider because ExternalBaseModel + // is defined in the same file and will be found in the customization compilation + Assert.IsInstanceOf(modelProvider.BaseTypeProvider, + "ExternalBaseModel is in the customization compilation and should use NamedTypeSymbolProvider"); + + // It should have the properties from the symbol + // ExternalBaseModel has 2 properties: ExternalProperty and ExternalDictionary + Assert.AreEqual(2, modelProvider.BaseTypeProvider!.Properties.Count, + "ExternalBaseModel should have ExternalProperty and ExternalDictionary"); + var externalPropertyNames = modelProvider.BaseTypeProvider.Properties.Select(p => p.Name).ToList(); + Assert.Contains("ExternalProperty", externalPropertyNames); + Assert.Contains("ExternalDictionary", externalPropertyNames); + } + + [Test] + public async Task CanCustomizeBaseModelToSystemType() + { + // This test verifies that a model can be customized to inherit from a system type + // (e.g., System.Exception) which simulates inheriting from types like + // Azure.ResourceManager.TrackedResourceData that are from referenced assemblies. + var childModel = InputFactory.Model( + "mockInputModel", + properties: [InputFactory.Property("childProp", InputPrimitiveType.String)], + usage: InputModelTypeUsage.Json); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [childModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel") as ModelProvider; + + // Should have customized base type from system library + Assert.IsNotNull(modelProvider); + Assert.IsNotNull(modelProvider!.BaseType); + Assert.IsNotNull(modelProvider.BaseTypeProvider); + Assert.AreEqual("Exception", modelProvider.BaseType!.Name); + Assert.AreEqual("System", modelProvider.BaseType!.Namespace); + + // The BaseModelProvider should be null since the base is not a generated model + Assert.IsNull(modelProvider.BaseModelProvider); + + // System types from referenced assemblies are found via GetTypeByMetadataName + // because the test compilation includes system assembly references + Assert.IsInstanceOf(modelProvider.BaseTypeProvider, + "System.Exception is found in the compilation and uses NamedTypeSymbolProvider"); + } + private class TestNameVisitor : NameVisitor { public TypeProvider? InvokeVisit(TypeProvider type) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeBaseModelToExternalType/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeBaseModelToExternalType/MockInputModel.cs new file mode 100644 index 00000000000..052967e40d0 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeBaseModelToExternalType/MockInputModel.cs @@ -0,0 +1,19 @@ +#nullable disable + +using Sample.Models; +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class MockInputModel : ExternalBaseModel + { + } + + // This simulates an external base type from another assembly/namespace + // that is not generated by the current generation run + public class ExternalBaseModel + { + public string ExternalProperty { get; set; } + public IDictionary ExternalDictionary { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeBaseModelToSystemType/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeBaseModelToSystemType/MockInputModel.cs new file mode 100644 index 00000000000..c1069c7824d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeBaseModelToSystemType/MockInputModel.cs @@ -0,0 +1,13 @@ +#nullable disable + +using System; + +namespace Sample.Models +{ + // This test case shows a model inheriting from a system type (System.Exception). + // This simulates inheriting from types like Azure.ResourceManager.TrackedResourceData + // which are from referenced assemblies. + public partial class MockInputModel : Exception + { + } +}