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 ae716cfd544..e508e72e1ba 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 @@ -142,11 +142,14 @@ private IReadOnlyList BuildDerivedModels() return existingProvider; } - // Try to find the type in the customization compilation (excluding referenced assemblies) + // 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); + baseType.DeclaringType?.Name, + includeReferencedAssemblies: true); if (baseTypeProvider != null) { @@ -155,8 +158,8 @@ private IReadOnlyList BuildDerivedModels() return baseTypeProvider; } - // If we couldn't find the type symbol (e.g., type is from a referenced assembly), - // create a SystemObjectTypeProvider that represents the external type + // 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; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs index 25c53b32916..179f8d32291 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/NamedTypeSymbolProvider.cs @@ -22,6 +22,7 @@ internal sealed class NamedTypeSymbolProvider : TypeProvider { private INamedTypeSymbol _namedTypeSymbol; private readonly Compilation _compilation; + private TypeProvider? _baseTypeProvider; public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation compilation) { @@ -41,21 +42,36 @@ public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation com protected override IReadOnlyList BuildAttributes() => [.._namedTypeSymbol.GetAttributes().Select(a => new AttributeStatement(a))]; + internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= BuildBaseTypeProvider(); + protected override CSharpType? BuildBaseType() { - if (_namedTypeSymbol.BaseType == null - || _namedTypeSymbol.BaseType.SpecialType == SpecialType.System_Object - || _namedTypeSymbol.BaseType.SpecialType == SpecialType.System_ValueType - || _namedTypeSymbol.BaseType.SpecialType == SpecialType.System_Array - || _namedTypeSymbol.BaseType.SpecialType == SpecialType.System_Enum - || TypeSymbolExtensions.ContainsTypeAsArgument(_namedTypeSymbol.BaseType, _namedTypeSymbol)) + if (ShouldSkipBaseType(_namedTypeSymbol.BaseType)) { return null; } - return _namedTypeSymbol.BaseType.GetCSharpType(); + return _namedTypeSymbol.BaseType!.GetCSharpType(); } + private TypeProvider? BuildBaseTypeProvider() + { + if (ShouldSkipBaseType(_namedTypeSymbol.BaseType)) + { + return null; + } + + return new NamedTypeSymbolProvider(_namedTypeSymbol.BaseType!, _compilation); + } + + private bool ShouldSkipBaseType(INamedTypeSymbol? baseType) + => baseType == null + || baseType.SpecialType == SpecialType.System_Object + || baseType.SpecialType == SpecialType.System_ValueType + || baseType.SpecialType == SpecialType.System_Array + || baseType.SpecialType == SpecialType.System_Enum + || TypeSymbolExtensions.ContainsTypeAsArgument(baseType, _namedTypeSymbol); + protected override TypeSignatureModifiers BuildDeclarationModifiers() { var declaredModifiers = GetAccessModifiers(_namedTypeSymbol.DeclaredAccessibility); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 5b1af563dff..e850150e8ad 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -64,13 +64,23 @@ private IReadOnlyList BuildAllCustomProperties() var allCustomProperties = CustomCodeView?.Properties != null ? new List(CustomCodeView.Properties) : []; - var baseTypeCustomCodeView = BaseTypeProvider?.CustomCodeView; + var baseTypeProvider = BaseTypeProvider; + var includeBaseProviderMembers = CustomCodeView?.BaseType != null; + var visited = new HashSet(); // add all custom properties from base types - while (baseTypeCustomCodeView != null) + while (baseTypeProvider != null && visited.Add(baseTypeProvider)) { - allCustomProperties.AddRange(baseTypeCustomCodeView.Properties); - baseTypeCustomCodeView = baseTypeCustomCodeView.BaseTypeProvider?.CustomCodeView; + if (includeBaseProviderMembers) + { + allCustomProperties.AddRange(baseTypeProvider.Properties); + } + + if (baseTypeProvider.CustomCodeView is { } customCodeView) + { + allCustomProperties.AddRange(customCodeView.Properties); + } + baseTypeProvider = baseTypeProvider.BaseTypeProvider; } return allCustomProperties; @@ -81,13 +91,23 @@ private IReadOnlyList BuildAllCustomFields() var allCustomFields = CustomCodeView?.Fields != null ? new List(CustomCodeView.Fields) : []; - var baseTypeCustomCodeView = BaseTypeProvider?.CustomCodeView; + var baseTypeProvider = BaseTypeProvider; + var includeBaseProviderMembers = CustomCodeView?.BaseType != null; + var visited = new HashSet(); // add all custom fields from base types - while (baseTypeCustomCodeView != null) + while (baseTypeProvider != null && visited.Add(baseTypeProvider)) { - allCustomFields.AddRange(baseTypeCustomCodeView.Fields); - baseTypeCustomCodeView = baseTypeCustomCodeView.BaseTypeProvider?.CustomCodeView; + if (includeBaseProviderMembers) + { + allCustomFields.AddRange(baseTypeProvider.Fields); + } + + if (baseTypeProvider.CustomCodeView is { } customCodeView) + { + allCustomFields.AddRange(customCodeView.Fields); + } + baseTypeProvider = baseTypeProvider.BaseTypeProvider; } return allCustomFields; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs index bd88dbe375c..a329166ee4b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/SourceInput/SourceInputModel.cs @@ -60,9 +60,9 @@ private IReadOnlyDictionary PopulateNameMap() return nameMap; } - public TypeProvider? FindForTypeInCustomization(string ns, string name, string? declaringTypeName = null) + public TypeProvider? FindForTypeInCustomization(string ns, string name, string? declaringTypeName = null, bool includeReferencedAssemblies = false) { - return FindTypeInCustomization(Customization, ns, name, false, declaringTypeName); + return FindTypeInCustomization(Customization, ns, name, includeReferencedAssemblies, declaringTypeName); } public TypeProvider? FindForTypeInLastContract(string ns, string name, string? declaringTypeName = null) 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 9033ed9b667..6fb38dc3961 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 @@ -1765,10 +1765,100 @@ public async Task CanCustomizeBaseModelToSystemType() // The BaseModelProvider should be null since the base is not a generated model Assert.IsNull(modelProvider.BaseModelProvider); - // System types from referenced assemblies are NOT found by FindForTypeInCustomization - // (which only searches the customization assembly, not references), so they use SystemObjectTypeProvider - Assert.IsInstanceOf(modelProvider.BaseTypeProvider, - "System.Exception is from a referenced assembly and should use SystemObjectTypeProvider"); + // System types from referenced assemblies are found in the customization compilation + // so inherited members can be represented by normal property providers. + Assert.IsInstanceOf(modelProvider.BaseTypeProvider, + "System.Exception is from a referenced assembly and should use NamedTypeSymbolProvider"); + } + + [Test] + public async Task CanCustomizeSpecBaseModelToSystemType() + { + // This verifies that a custom partial base type wins even when the input model + // has a TypeSpec base model. Otherwise the generated partial would keep the + // TypeSpec base and conflict with the custom partial declaration. + var specBaseModel = InputFactory.Model( + "specBaseModel", + properties: [InputFactory.Property("specBaseProp", InputPrimitiveType.String)], + usage: InputModelTypeUsage.Json); + var childModel = InputFactory.Model( + "mockInputModel", + properties: [ + InputFactory.Property("message", InputPrimitiveType.String), + InputFactory.Property("childProp", InputPrimitiveType.String), + ], + baseModel: specBaseModel, + usage: InputModelTypeUsage.Json); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [childModel, specBaseModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel") as ModelProvider; + + Assert.IsNotNull(modelProvider); + Assert.IsNotNull(modelProvider!.BaseType); + Assert.AreEqual("Exception", modelProvider.BaseType!.Name); + Assert.AreEqual("System", modelProvider.BaseType!.Namespace); + Assert.IsNull(modelProvider.BaseModelProvider, "The TypeSpec base model should not be used when custom code declares a system base type."); + Assert.IsInstanceOf(modelProvider.BaseTypeProvider); + Assert.That(modelProvider.Properties.Select(p => p.Name), Does.Not.Contain("Message")); + Assert.That(modelProvider.Properties.Select(p => p.Name), Does.Contain("ChildProp")); + + var modelContent = new TypeProviderWriter(modelProvider).Write().Content; + Assert.That(modelContent, Does.Contain("public partial class MockInputModel : global::System.Exception")); + Assert.That(modelContent, Does.Not.Contain("SpecBaseModel")); + Assert.That(modelContent, Does.Not.Contain("public string Message")); + } + + [Test] + public async Task CanCustomizeSpecBaseModelToSystemObjectModelProvider() + { + // This verifies the generator-specific system model path used by management-plane + // generators: a custom base type can resolve to a SystemObjectModelProvider in + // CSharpTypeMap, and generated members inherited from that mapped provider are filtered. + var specBaseModel = InputFactory.Model( + "specBaseModel", + properties: [InputFactory.Property("specBaseProp", InputPrimitiveType.String)], + usage: InputModelTypeUsage.Json); + var childModel = InputFactory.Model( + "mockInputModel", + properties: [ + InputFactory.Property("id", InputPrimitiveType.String), + InputFactory.Property("name", InputPrimitiveType.String), + InputFactory.Property("childProp", InputPrimitiveType.String), + ], + baseModel: specBaseModel, + usage: InputModelTypeUsage.Json); + var systemInputModel = InputFactory.Model( + "ResourceData", + properties: [ + InputFactory.Property("id", InputPrimitiveType.String), + InputFactory.Property("name", InputPrimitiveType.String), + ], + usage: InputModelTypeUsage.Json); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [childModel, specBaseModel, systemInputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var customBaseType = CreateSystemCSharpType("ResourceData", "TestFramework"); + CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[customBaseType] = new SystemObjectModelProvider(customBaseType, systemInputModel); + + var modelProvider = new ModelProvider(childModel); + + Assert.IsNotNull(modelProvider.BaseType); + Assert.AreEqual("ResourceData", modelProvider.BaseType!.Name); + Assert.AreEqual("TestFramework", modelProvider.BaseType!.Namespace); + Assert.IsInstanceOf(modelProvider.BaseTypeProvider); + Assert.That(modelProvider.Properties.Select(p => p.Name), Does.Not.Contain("Id")); + Assert.That(modelProvider.Properties.Select(p => p.Name), Does.Not.Contain("Name")); + Assert.That(modelProvider.Properties.Select(p => p.Name), Does.Contain("ChildProp")); + + var modelContent = new TypeProviderWriter(modelProvider).Write().Content; + Assert.That(modelContent, Does.Contain("public partial class MockInputModel : global::TestFramework.ResourceData")); + Assert.That(modelContent, Does.Not.Contain("public string Id")); + Assert.That(modelContent, Does.Not.Contain("public string Name")); } [Test] @@ -1847,6 +1937,10 @@ await MockHelpers.LoadMockGeneratorAsync( private const string AttributeNamespace = TestCustomCodeAttributeDefinition.AttributeNamespace; + private static CSharpType CreateSystemCSharpType(string name, string ns) + => new(name, ns, isValueType: false, isNullable: false, declaringType: null, + args: Array.Empty(), isPublic: true, isStruct: false); + 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/CanCustomizeSpecBaseModelToSystemObjectModelProvider/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeSpecBaseModelToSystemObjectModelProvider/MockInputModel.cs new file mode 100644 index 00000000000..93ce98e007f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeSpecBaseModelToSystemObjectModelProvider/MockInputModel.cs @@ -0,0 +1,15 @@ +#nullable disable + +namespace TestFramework +{ + public class ResourceData + { + } +} + +namespace Sample.Models +{ + public partial class MockInputModel : TestFramework.ResourceData + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeSpecBaseModelToSystemType/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeSpecBaseModelToSystemType/MockInputModel.cs new file mode 100644 index 00000000000..0af635cabf2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanCustomizeSpecBaseModelToSystemType/MockInputModel.cs @@ -0,0 +1,10 @@ +#nullable disable + +using System; + +namespace Sample.Models +{ + public partial class MockInputModel : Exception + { + } +}