diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Xml.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Xml.cs index 5d76b8f44a1..11da0d059db 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Xml.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Xml.cs @@ -61,13 +61,83 @@ private record XmlPropertyCategories( private XmlPropertyCategories? _categorizedXmlProperties; private XmlPropertyCategories CategorizedXmlProperties => _categorizedXmlProperties ??= CategorizeXmlProperties(ownPropertiesOnly: true); + private bool? _shouldOverrideXmlMethods; + private bool ShouldOverrideXmlMethods => _shouldOverrideXmlMethods ??= !_isStruct && + (HasGeneratedBaseSerializationMethod(XmlModelWriteCoreMethodName) || + HasGeneratedBaseCustomXmlModelWriteCoreMethod() || + HasCustomBaseXmlModelWriteCoreMethod()); + + private bool HasGeneratedBaseSerializationMethod(string methodName) + => _model.BaseModelProvider is not null && + _model.BaseModelProvider is not SystemObjectModelProvider && + _model.BaseModelProvider.SerializationProviders + .OfType() + .Any(serialization => serialization.Methods.Any(method => method.Signature.Name == methodName)); + + private bool HasGeneratedBaseCustomXmlModelWriteCoreMethod() + => _model.BaseModelProvider is not null && + _model.BaseModelProvider is not SystemObjectModelProvider && + _model.BaseModelProvider.CustomCodeView?.Methods.Any(IsXmlModelWriteCoreMethod) == true; + + private bool IsXmlModelWriteCoreMethod(MethodProvider method) + => IsXmlModelWriteCoreSignature(method) && + HasInternalOnlyAccessibility(method.Signature.Modifiers) && + IsOverridable(method.Signature.Modifiers); + + private static bool HasInternalOnlyAccessibility(MethodSignatureModifiers modifiers) + => modifiers.HasFlag(MethodSignatureModifiers.Internal) && + !modifiers.HasFlag(MethodSignatureModifiers.Public) && + !modifiers.HasFlag(MethodSignatureModifiers.Protected) && + !modifiers.HasFlag(MethodSignatureModifiers.Private); + + private bool IsXmlModelWriteCoreSignature(MethodProvider method) + => method.Signature.Name == XmlModelWriteCoreMethodName && + !method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Static) && + (method.Signature.ReturnType is null || method.Signature.ReturnType.Equals(typeof(void))) && + method.Signature.Parameters.Count == 2 && + method.Signature.Parameters[0].Type.Equals(typeof(XmlWriter)) && + method.Signature.Parameters[1].Type.Equals(typeof(ModelReaderWriterOptions)); + + private static bool IsOverridable(MethodSignatureModifiers modifiers) + => !modifiers.HasFlag(MethodSignatureModifiers.Sealed) && + (modifiers.HasFlag(MethodSignatureModifiers.Virtual) || + modifiers.HasFlag(MethodSignatureModifiers.Override) || + modifiers.HasFlag(MethodSignatureModifiers.Abstract)); + + private bool HasCustomBaseXmlModelWriteCoreMethod() + => GetCustomSerializationBaseType() is not null && + _model.BaseType is { } baseType && + HasCompatibleXmlModelWriteCoreInHierarchy(baseType, []); + + private bool HasCompatibleXmlModelWriteCoreInHierarchy(CSharpType type, HashSet visited) + { + if (!visited.Add(type.FullyQualifiedName)) + { + return false; + } + + var provider = TryGetTypeProvider(type); + if (provider is not null && GetXmlModelWriteCoreCompatibility(provider) is { } isCompatible) + { + return isCompatible; + } + + var baseType = provider?.BaseType ?? type.BaseType; + return baseType is not null && HasCompatibleXmlModelWriteCoreInHierarchy(baseType, visited); + } + + private bool? GetXmlModelWriteCoreCompatibility(TypeProvider type) + { + var sourceMethod = type.Methods.FirstOrDefault(IsXmlModelWriteCoreSignature); + return sourceMethod is null ? null : IsXmlModelWriteCoreMethod(sourceMethod); + } private MethodProvider BuildXmlModelWriteCoreMethod() { MethodSignatureModifiers modifiers = _isStruct ? MethodSignatureModifiers.Private : MethodSignatureModifiers.Internal | MethodSignatureModifiers.Virtual; - if (_shouldOverrideXmlMethods) + if (ShouldOverrideXmlMethods) { modifiers = MethodSignatureModifiers.Internal | MethodSignatureModifiers.Override; } @@ -81,7 +151,7 @@ private MethodProvider BuildXmlModelWriteCoreMethod() private MethodBodyStatement[] BuildXmlModelWriteCoreMethodBody() { - var categorizedProperties = _shouldOverrideXmlMethods + var categorizedProperties = ShouldOverrideXmlMethods ? CategorizedXmlProperties : AllCategorizedXmlProperties; var statements = new List @@ -90,7 +160,7 @@ private MethodBodyStatement[] BuildXmlModelWriteCoreMethodBody() MethodBodyStatement.EmptyLine }; - if (_shouldOverrideXmlMethods) + if (ShouldOverrideXmlMethods) { statements.Add(Base.Invoke(XmlModelWriteCoreMethodName, _xmlWriterParameter, _serializationOptionsParameter).Terminate()); } 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 8202a2af405..6e9318310ca 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 @@ -74,10 +74,10 @@ public partial class MrwSerializationTypeDefinition : TypeProvider private ConstructorProvider? _serializationConstructor; // Flag to determine if the model should override the serialization methods private bool? _shouldOverrideMethods; - private bool ShouldOverrideMethods => _shouldOverrideMethods ??= _model.BaseModelProvider != null && !_isStruct; + private bool ShouldOverrideMethods => _shouldOverrideMethods ??= !_isStruct && + (_model.BaseModelProvider != null || HasCustomBaseMethod(JsonModelWriteCoreMethodName)); private bool? _shouldSkipSerializationMethodOverrides; - private bool ShouldSkipSerializationMethodOverrides => _shouldSkipSerializationMethodOverrides ??= ShouldSkipDerivedSerializationMethodOverrides(_model.BaseModelProvider); - private readonly bool _shouldOverrideXmlMethods; + private bool ShouldSkipSerializationMethodOverrides => _shouldSkipSerializationMethodOverrides ??= ShouldSkipDerivedSerializationMethodOverrides(_model); private readonly Lazy _additionalProperties; // Unknown discriminator models use their base model as the serialization interface type. @@ -100,7 +100,6 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m _isStruct = _model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Struct); _supportsXml = inputModel.Usage.HasFlag(InputModelTypeUsage.Xml); _supportsJson = inputModel.Usage.HasFlag(InputModelTypeUsage.Json) || !_supportsXml; - _shouldOverrideXmlMethods = _model.BaseModelProvider != null && !_isStruct; _rawDataField = _model.Fields.FirstOrDefault(f => f.Name == AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName); _additionalBinaryDataProperty = new(GetAdditionalBinaryDataPropertiesProp); _additionalProperties = new(() => [.. _model.Properties.Where(p => p.IsAdditionalProperties)]); @@ -152,7 +151,15 @@ private CSharpType GetRootModelType() { // We need to explicitly use the BaseModelProvider when looking up the root type // to account for any customizations that may have changed the base model. - var returnType = _model.BaseModelProvider?.Type ?? Type; + var returnType = _model.BaseModelProvider?.Type ?? + GetCustomBaseRootType() ?? + GetCustomBaseMethodReturnType(new HashSet + { + JsonModelCreateCoreMethodName, + PersistableModelCreateCoreMethodName + }) ?? + GetCustomSerializationBaseType() ?? + Type; while (returnType.BaseType != null && IsModelType(returnType.BaseType)) { @@ -180,43 +187,155 @@ private static bool IsModelType(CSharpType type) /// When it does not (for example a hand-authored base such as ResourceData), derived /// models re-introduce the methods as virtual. /// - private static bool ShouldSkipDerivedSerializationMethodOverrides(ModelProvider? baseModelProvider) + private CSharpType? GetCustomSerializationBaseType() + => _model.BaseModelProvider is null && _model.CustomCodeView?.BaseType is not null + ? _model.BaseType + : null; + + private CSharpType? GetCustomBaseRootType() + => GetCustomSerializationBaseType() is { } baseType ? GetRootBaseType(baseType) : null; + + private bool HasCustomBaseMethod(string methodName) + => GetCustomSerializationBaseType() is { } baseType && + HasMethodInTypeHierarchy(baseType, methodName); + + private CSharpType? GetCustomBaseMethodReturnType(IReadOnlySet methodNames) + => GetCustomSerializationBaseType() is { } baseType + ? GetMethodReturnTypeInHierarchy(baseType, methodNames) + : null; + + private static bool ShouldSkipDerivedSerializationMethodOverrides(ModelProvider model) { - if (baseModelProvider is null) + if (model.BaseModelProvider is null) { - return false; + return model.CustomCodeView?.BaseType is null || + !HasMethodInTypeHierarchy(model.BaseType!, JsonModelCreateCoreMethodName) && + !HasMethodInTypeHierarchy(model.BaseType!, PersistableModelCreateCoreMethodName); } - if (baseModelProvider is SystemObjectModelProvider systemBase) + if (model.BaseModelProvider is SystemObjectModelProvider systemBase) { return !SystemTypeImplementsModelReaderWriter(systemBase.SystemType); } - return baseModelProvider.ShouldSkipDerivedSerializationMethodOverrides; + return model.BaseModelProvider.ShouldSkipDerivedSerializationMethodOverrides; } - private static bool SystemTypeImplementsModelReaderWriter(CSharpType systemType) + private static CSharpType? GetRootBaseType(CSharpType baseType) { - if (!systemType.IsFrameworkType) + var visited = new HashSet(); + CSharpType? rootBaseType = null; + var current = baseType; + + while (current is not null && visited.Add(current.FullyQualifiedName)) { - return false; + if (!IsFrameworkRootType(current)) + { + rootBaseType = current; + } + + current = current.BaseType; + } + + return rootBaseType; + } + + private static bool HasMethodInTypeHierarchy(CSharpType type, string methodName) + => GetMethodReturnTypeInHierarchy(type, new HashSet { methodName }) is not null; + + private static CSharpType? GetMethodReturnTypeInHierarchy(CSharpType type, IReadOnlySet methodNames) + { + var visited = new HashSet(); + return GetMethodReturnTypeInHierarchy(type, methodNames, visited); + } + + private static CSharpType? GetMethodReturnTypeInHierarchy(CSharpType type, IReadOnlySet methodNames, HashSet visited) + { + if (!visited.Add(type.FullyQualifiedName)) + { + return null; } - foreach (var @interface in systemType.FrameworkType.GetInterfaces()) + if (type.BaseType is not null && + GetMethodReturnTypeInHierarchy(type.BaseType, methodNames, visited) is { } baseReturnType) { - if (@interface.IsGenericType) + return baseReturnType; + } + + foreach (var methodName in methodNames) + { + if (TryGetMethodReturnType(type, methodName) is { } returnType) { - var definition = @interface.GetGenericTypeDefinition(); - if (definition == typeof(IJsonModel<>) || definition == typeof(IPersistableModel<>)) - { - return true; - } + return returnType; } } - return false; + return null; + } + + private static CSharpType? TryGetMethodReturnType(CSharpType type, string methodName) + { + if (TryGetTypeProvider(type) is { } referencedType) + { + return referencedType.Methods.FirstOrDefault(method => method.Signature.Name == methodName)?.Signature.ReturnType; + } + + return null; + } + + private static TypeProvider? TryGetTypeProvider(CSharpType type) + { + if (TryGetMappedTypeProvider(type) is { } provider) + { + return provider; + } + + return TryGetReferencedType(type); + } + + private static TypeProvider? TryGetMappedTypeProvider(CSharpType type) + { + foreach (var (mappedType, provider) in ScmCodeModelGenerator.Instance.TypeFactory.CSharpTypeMap) + { + if (mappedType.FullyQualifiedName == type.FullyQualifiedName || + provider is SystemObjectModelProvider systemProvider && + systemProvider.SystemType.FullyQualifiedName == type.FullyQualifiedName) + { + return provider; + } + } + + return null; + } + + private static TypeProvider? TryGetReferencedType(CSharpType type) + => string.IsNullOrEmpty(type.Namespace) + ? null + : CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( + type.Namespace, + type.Name, + declaringTypeName: type.DeclaringType?.Name, + includeReferencedAssemblies: true); + + private static bool IsFrameworkRootType(CSharpType type) + => (type.IsFrameworkType && + (type.FrameworkType == typeof(object) || type.FrameworkType == typeof(ValueType))) || + (type.Namespace == "System" && (type.Name == "Object" || type.Name == "ValueType")); + + private static bool SystemTypeImplementsModelReaderWriter(CSharpType systemType) + { + if (TryGetMappedTypeProvider(systemType) is not { } systemTypeProvider) + { + return false; + } + + return systemTypeProvider.Implements.Any(IsModelReaderWriterInterface); } + private static bool IsModelReaderWriterInterface(CSharpType type) + => type.Namespace == typeof(IJsonModel<>).Namespace && + (type.Name == nameof(IJsonModel) || type.Name == nameof(IPersistableModel)); + protected override ConstructorProvider[] BuildConstructors() { List constructors = new(); 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..0514e1dd24b 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 @@ -246,7 +246,7 @@ private static (ModelProvider Model, MrwSerializationTypeDefinition Serializatio // The base wraps a framework type that declares the generated MRW *Core methods, so a // derived model must override them (mirrors deriving from an external MRW-generated type). var systemType = new CSharpType(typeof(FakeMrwBase)); - var systemBase = new SystemObjectModelProvider(systemType, baseInputModel); + var systemBase = new FakeMrwSystemObjectModelProvider(systemType, baseInputModel); var generator = MockHelpers.LoadMockGenerator( inputModels: () => [baseInputModel, derivedInputModel], @@ -354,6 +354,19 @@ string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOpti => "J"; } + private class FakeMrwSystemObjectModelProvider : SystemObjectModelProvider + { + private readonly CSharpType _type; + + public FakeMrwSystemObjectModelProvider(CSharpType type, InputModelType inputModel) : base(type, inputModel) + { + _type = type; + } + + protected internal override CSharpType[] BuildImplements() + => [new CSharpType(typeof(IJsonModel<>), _type)]; + } + private class DelayedBaseModelProvider(InputModelType inputModel) : ModelProvider(inputModel) { public ModelProvider? BaseModel { get; set; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithNonVirtualCustomMethod/BaseModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithNonVirtualCustomMethod/BaseModel.cs new file mode 100644 index 00000000000..edbbf75ae94 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithNonVirtualCustomMethod/BaseModel.cs @@ -0,0 +1,13 @@ +#nullable disable + +using System.ClientModel.Primitives; +using System.Xml; + +namespace Sample.Models; + +public partial class BaseModel +{ + internal void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithProtectedCustomMethod/BaseModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithProtectedCustomMethod/BaseModel.cs new file mode 100644 index 00000000000..180b8548502 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithProtectedCustomMethod/BaseModel.cs @@ -0,0 +1,13 @@ +#nullable disable + +using System.ClientModel.Primitives; +using System.Xml; + +namespace Sample.Models; + +public partial class BaseModel +{ + protected virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithSealedCustomMethod/BaseModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithSealedCustomMethod/BaseModel.cs new file mode 100644 index 00000000000..3657b063eda --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithSealedCustomMethod/BaseModel.cs @@ -0,0 +1,20 @@ +#nullable disable + +using System.ClientModel.Primitives; +using System.Xml; + +namespace Sample.Models; + +public partial class BaseModel : BaseModelXmlHook +{ + internal sealed override void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + } +} + +public class BaseModelXmlHook +{ + internal virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideJsonOnlyCustomBase/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideJsonOnlyCustomBase/MockInputModel.cs new file mode 100644 index 00000000000..28a7c8aee8a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideJsonOnlyCustomBase/MockInputModel.cs @@ -0,0 +1,17 @@ +#nullable disable + +using System.ClientModel.Primitives; +using System.Text.Json; + +namespace Sample.Models; + +public partial class MockInputModel : JsonOnlyBase +{ +} + +public class JsonOnlyBase +{ + protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideProtectedCustomExternalBase/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideProtectedCustomExternalBase/MockInputModel.cs new file mode 100644 index 00000000000..44718db7161 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideProtectedCustomExternalBase/MockInputModel.cs @@ -0,0 +1,17 @@ +#nullable disable + +using System.ClientModel.Primitives; +using System.Xml; + +namespace Sample.Models; + +public partial class MockInputModel : ProtectedXmlBase +{ +} + +public class ProtectedXmlBase +{ + protected virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideProtectedInternalCustomExternalBase/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideProtectedInternalCustomExternalBase/MockInputModel.cs new file mode 100644 index 00000000000..07ae8c5f006 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreDoesNotOverrideProtectedInternalCustomExternalBase/MockInputModel.cs @@ -0,0 +1,17 @@ +#nullable disable + +using System.ClientModel.Primitives; +using System.Xml; + +namespace Sample.Models; + +public partial class MockInputModel : ProtectedInternalXmlBase +{ +} + +public class ProtectedInternalXmlBase +{ + protected internal virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreOverridesGeneratedBaseWithCustomReplacement/BaseModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreOverridesGeneratedBaseWithCustomReplacement/BaseModel.cs new file mode 100644 index 00000000000..0852b6b93af --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationCustomizationTests/XmlModelWriteCoreOverridesGeneratedBaseWithCustomReplacement/BaseModel.cs @@ -0,0 +1,13 @@ +#nullable disable + +using System.ClientModel.Primitives; +using System.Xml; + +namespace Sample.Models; + +public partial class BaseModel +{ + internal virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationCustomizationTests.cs index e85c5a4c46d..6965519f773 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationCustomizationTests.cs @@ -89,6 +89,216 @@ public async Task CanChangePropertyName() Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + [Test] + public async Task XmlModelWriteCoreDoesNotOverrideJsonOnlyCustomBase() + { + var inputModel = InputFactory.Model( + "mockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + properties: + [ + InputFactory.Property("Prop1", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("prop1"))) + ]); + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider); + Assert.AreEqual("JsonOnlyBase", modelProvider.BaseType?.Name); + + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + var xmlWriteCore = serializationProvider.Methods.Single(m => m.Signature.Name == "XmlModelWriteCore"); + + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)); + Assert.IsFalse(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override)); + StringAssert.DoesNotContain("base.XmlModelWriteCore", xmlWriteCore.BodyStatements!.ToDisplayString()); + } + + [Test] + public async Task XmlModelWriteCoreDoesNotOverrideProtectedCustomExternalBase() + { + var inputModel = InputFactory.Model( + "mockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + properties: + [ + InputFactory.Property("Prop1", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("prop1"))) + ]); + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider); + Assert.AreEqual("ProtectedXmlBase", modelProvider.BaseType?.Name); + + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + var xmlWriteCore = serializationProvider.Methods.Single(m => m.Signature.Name == "XmlModelWriteCore"); + + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)); + Assert.IsFalse(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override)); + StringAssert.DoesNotContain("base.XmlModelWriteCore", xmlWriteCore.BodyStatements!.ToDisplayString()); + } + + [Test] + public async Task XmlModelWriteCoreDoesNotOverrideProtectedInternalCustomExternalBase() + { + var inputModel = InputFactory.Model( + "mockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + properties: + [ + InputFactory.Property("Prop1", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("prop1"))) + ]); + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t is ModelProvider); + Assert.AreEqual("ProtectedInternalXmlBase", modelProvider.BaseType?.Name); + + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + var xmlWriteCore = serializationProvider.Methods.Single(m => m.Signature.Name == "XmlModelWriteCore"); + + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)); + Assert.IsFalse(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override)); + StringAssert.DoesNotContain("base.XmlModelWriteCore", xmlWriteCore.BodyStatements!.ToDisplayString()); + } + + [Test] + public async Task XmlModelWriteCoreOverridesGeneratedBaseWithCustomReplacement() + { + var baseModel = InputFactory.Model( + "baseModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + properties: + [ + InputFactory.Property("BaseProp", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("baseProp"))) + ]); + var inputModel = InputFactory.Model( + "mockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + baseModel: baseModel, + properties: + [ + InputFactory.Property("Prop1", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("prop1"))) + ]); + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [baseModel, inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + var xmlWriteCore = serializationProvider.Methods.Single(m => m.Signature.Name == "XmlModelWriteCore"); + + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override)); + Assert.IsFalse(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)); + StringAssert.Contains("base.XmlModelWriteCore", xmlWriteCore.BodyStatements!.ToDisplayString()); + } + + [Test] + public async Task XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithNonVirtualCustomMethod() + { + var baseModel = InputFactory.Model( + "baseModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + properties: + [ + InputFactory.Property("BaseProp", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("baseProp"))) + ]); + var inputModel = InputFactory.Model( + "mockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + baseModel: baseModel, + properties: + [ + InputFactory.Property("Prop1", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("prop1"))) + ]); + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [baseModel, inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + var xmlWriteCore = serializationProvider.Methods.Single(m => m.Signature.Name == "XmlModelWriteCore"); + + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)); + Assert.IsFalse(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override)); + StringAssert.DoesNotContain("base.XmlModelWriteCore", xmlWriteCore.BodyStatements!.ToDisplayString()); + } + + [Test] + public async Task XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithProtectedCustomMethod() + { + var baseModel = InputFactory.Model( + "baseModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + properties: + [ + InputFactory.Property("BaseProp", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("baseProp"))) + ]); + var inputModel = InputFactory.Model( + "mockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + baseModel: baseModel, + properties: + [ + InputFactory.Property("Prop1", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("prop1"))) + ]); + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [baseModel, inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + var xmlWriteCore = serializationProvider.Methods.Single(m => m.Signature.Name == "XmlModelWriteCore"); + + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)); + Assert.IsFalse(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override)); + StringAssert.DoesNotContain("base.XmlModelWriteCore", xmlWriteCore.BodyStatements!.ToDisplayString()); + } + + [Test] + public async Task XmlModelWriteCoreDoesNotOverrideGeneratedBaseWithSealedCustomMethod() + { + var baseModel = InputFactory.Model( + "baseModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + properties: + [ + InputFactory.Property("BaseProp", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("baseProp"))) + ]); + var inputModel = InputFactory.Model( + "mockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml, + baseModel: baseModel, + properties: + [ + InputFactory.Property("Prop1", InputPrimitiveType.String, serializationOptions: InputFactory.Serialization.Options(xml: InputFactory.Serialization.Xml("prop1"))) + ]); + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => [baseModel, inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var baseModelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "BaseModel"); + var customBaseMethod = baseModelProvider.CustomCodeView!.Methods.Single(m => m.Signature.Name == "XmlModelWriteCore"); + Assert.IsTrue(customBaseMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Sealed)); + + var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + var serializationProvider = modelProvider.SerializationProviders.Single(t => t is MrwSerializationTypeDefinition); + var xmlWriteCore = serializationProvider.Methods.Single(m => m.Signature.Name == "XmlModelWriteCore"); + + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.IsTrue(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)); + Assert.IsFalse(xmlWriteCore.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override)); + StringAssert.DoesNotContain("base.XmlModelWriteCore", xmlWriteCore.BodyStatements!.ToDisplayString()); + } + // Validates that a custom deserialization hook can be used to customize property deserialization. [Test] public async Task CanCustomizeDeserializationMethod() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/MethodSignatureModifiers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/MethodSignatureModifiers.cs index 77c755b3299..dca28b18e71 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/MethodSignatureModifiers.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/MethodSignatureModifiers.cs @@ -23,6 +23,7 @@ public enum MethodSignatureModifiers Explicit = 2048, Implicit = 4096, Partial = 8192, - Abstract = 16384 + Abstract = 16384, + Sealed = 32768 } } 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..bce57ccb41a 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 @@ -214,6 +214,7 @@ protected FieldProvider? RawDataField } } protected virtual bool ShouldSkipDerivedModelProperties => false; + protected virtual bool ShouldUseFullConstructorInDerivedTypes => true; /// /// Gets whether derived models should skip overriding serialization methods from this base model. /// @@ -971,7 +972,7 @@ private IEnumerable GetAllBaseFieldsForConstructorInitialization( baseProperties = GetAllBasePropertiesForConstructorInitialization(includeDiscriminatorParameter); baseFields = GetAllBaseFieldsForConstructorInitialization(); } - else if (BaseModelProvider is not null && !HasBaseModelProviderCycle()) + else if (BaseModelProvider is not null && BaseModelProvider.ShouldUseFullConstructorInDerivedTypes && !HasBaseModelProviderCycle()) { baseParameters.AddRange(BaseModelProvider.FullConstructor.Signature.Parameters); } 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 ed3a45d54e8..4591a4b39a0 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 @@ -42,26 +42,29 @@ public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol, Compilation com protected override IReadOnlyList BuildAttributes() => [.._namedTypeSymbol.GetAttributes().Select(a => new AttributeStatement(a))]; + protected internal override CSharpType[] BuildImplements() + => [.. _namedTypeSymbol.AllInterfaces.Select(i => i.GetCSharpType())]; + internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= BuildBaseTypeProvider(); - protected override CSharpType? BuildBaseType() + private TypeProvider? BuildBaseTypeProvider() { if (ShouldSkipBaseType(_namedTypeSymbol.BaseType)) { return null; } - return _namedTypeSymbol.BaseType!.GetCSharpType(); + return new NamedTypeSymbolProvider(_namedTypeSymbol.BaseType!, _compilation); } - private TypeProvider? BuildBaseTypeProvider() + protected override CSharpType? BuildBaseType() { if (ShouldSkipBaseType(_namedTypeSymbol.BaseType)) { return null; } - return new NamedTypeSymbolProvider(_namedTypeSymbol.BaseType!, _compilation); + return _namedTypeSymbol.BaseType!.GetCSharpType(); } private bool ShouldSkipBaseType(INamedTypeSymbol? baseType) @@ -365,6 +368,10 @@ private void AddAdditionalModifiers(IMethodSymbol methodSymbol, ref MethodSignat { modifiers |= MethodSignatureModifiers.Override; } + if (methodSymbol.IsSealed) + { + modifiers |= MethodSignatureModifiers.Sealed; + } if (methodSymbol.IsAsync) { modifiers |= MethodSignatureModifiers.Async; @@ -510,6 +517,8 @@ private static XDocument ParseXml(ISymbol docsSymbol, string xmlDocumentation) Accessibility.Protected => MethodSignatureModifiers.Protected, Accessibility.Internal => MethodSignatureModifiers.Internal, Accessibility.Public => MethodSignatureModifiers.Public, + Accessibility.ProtectedOrInternal => MethodSignatureModifiers.Protected | MethodSignatureModifiers.Internal, + Accessibility.ProtectedAndInternal => MethodSignatureModifiers.Protected | MethodSignatureModifiers.Private, _ => MethodSignatureModifiers.None }; 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..1c8c5f6e640 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 @@ -20,15 +20,26 @@ namespace Microsoft.TypeSpec.Generator.Providers public class SystemObjectModelProvider : ModelProvider { private readonly CSharpType _systemType; + private readonly bool _skipDerivedConstructorParameters; /// /// 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) + public SystemObjectModelProvider(CSharpType systemType, InputModelType inputModel) + : this(systemType, inputModel, skipDerivedConstructorParameters: false) + { + } + + public SystemObjectModelProvider( + CSharpType systemType, + InputModelType inputModel, + bool skipDerivedConstructorParameters) + : base(inputModel) { _systemType = systemType ?? throw new ArgumentNullException(nameof(systemType)); + _skipDerivedConstructorParameters = skipDerivedConstructorParameters; CrossLanguageDefinitionId = inputModel.CrossLanguageDefinitionId; // The base ModelProvider constructor can evaluate Type before _systemType is assigned. @@ -58,6 +69,12 @@ protected override string BuildRelativeFilePath() // _systemType may be null when called from base constructor before field assignment. protected override string BuildNamespace() => _systemType?.Namespace ?? string.Empty; + /// + protected override CSharpType? BuildBaseType() => SystemType.BaseType ?? base.BuildBaseType(); + + /// + protected override bool ShouldUseFullConstructorInDerivedTypes => !_skipDerivedConstructorParameters; + /// protected override bool ShouldSkipDerivedModelProperties => true; 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 3d71670d5a9..a1381e7c6b7 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 @@ -9,6 +9,7 @@ using Microsoft.TypeSpec.Generator.EmitterRpc; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Statements; @@ -319,6 +320,7 @@ internal PropertyProvider[] FilterCustomizedProperties(IEnumerable(); var customProperties = new HashSet(); + var customWirePaths = new HashSet(); foreach (var customProperty in BuildAllCustomProperties()) { @@ -327,6 +329,10 @@ internal PropertyProvider[] FilterCustomizedProperties(IEnumerable customWirePaths) + { + if (!TryGetWirePath(property, out var wirePath) || !customWirePaths.Contains(wirePath)) + { + return false; + } + + return property.Name == wirePath.ToIdentifierName(); + } + + private static bool TryGetWirePath(PropertyProvider property, out string wirePath) + { + if (property.WireInfo is not null) + { + wirePath = property.WireInfo.SerializedName; + return true; + } + + foreach (var attribute in property.Attributes) + { + if (attribute.Type.Name is "WirePath" or "WirePathAttribute" && + attribute.Arguments is [LiteralExpression { Literal: string value }, ..]) + { + wirePath = value; + return true; + } + } + + wirePath = string.Empty; + return false; + } + internal FieldProvider[] FilterCustomizedFields(IEnumerable specFields) { var fields = new List(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs index 1e25bccefce..eeb1c57a30d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs @@ -340,6 +340,7 @@ public void WriteProperty(PropertyProvider property) .AppendRawIf("internal ", modifiers.HasFlag(MethodSignatureModifiers.Internal)) .AppendRawIf("private ", modifiers.HasFlag(MethodSignatureModifiers.Private)) .AppendRawIf("new ", modifiers.HasFlag(MethodSignatureModifiers.New)) + .AppendRawIf("sealed ", modifiers.HasFlag(MethodSignatureModifiers.Sealed)) .AppendRawIf("override ", modifiers.HasFlag(MethodSignatureModifiers.Override)) .AppendRawIf("static ", modifiers.HasFlag(MethodSignatureModifiers.Static)) .AppendRawIf("virtual ", modifiers.HasFlag(MethodSignatureModifiers.Virtual)); @@ -837,6 +838,7 @@ public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase, { AppendRawIf("virtual ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)) .AppendRawIf("abstract ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Abstract)) + .AppendRawIf("sealed ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Sealed)) .AppendRawIf("override ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Override)) .AppendRawIf("new ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.New)) .AppendRawIf("async ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Async)); 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 6fb38dc3961..dd109555f3c 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 @@ -1861,6 +1861,42 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.That(modelContent, Does.Not.Contain("public string Name")); } + [Test] + public async Task CanFilterCanonicalDuplicateWirePathFromSystemObjectModelProviderBase() + { + var childModel = InputFactory.Model( + "mockInputModel", + properties: + [ + InputFactory.Property("type", InputPrimitiveType.String, wireName: "type"), + InputFactory.Property("defaultName", InputPrimitiveType.String, wireName: "name"), + InputFactory.Property("childProp", InputPrimitiveType.String), + ], + usage: InputModelTypeUsage.Json); + var systemInputModel = InputFactory.Model( + "ResourceData", + properties: + [ + InputFactory.Property("resourceType", InputPrimitiveType.String, wireName: "type"), + InputFactory.Property("name", InputPrimitiveType.String, wireName: "name"), + ], + usage: InputModelTypeUsage.Json); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [childModel, 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); + var propertyNames = modelProvider.Properties.Select(p => p.Name).ToArray(); + + Assert.That(propertyNames, Does.Not.Contain("Type")); + Assert.That(propertyNames, Does.Contain("DefaultName")); + Assert.That(propertyNames, Does.Contain("ChildProp")); + } + [Test] public async Task CanReadPropertyAttributes() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanFilterCanonicalDuplicateWirePathFromSystemObjectModelProviderBase/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CanFilterCanonicalDuplicateWirePathFromSystemObjectModelProviderBase/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/CanFilterCanonicalDuplicateWirePathFromSystemObjectModelProviderBase/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/NamedTypeSymbolProviders/InterfaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/InterfaceTests.cs index 5cd0511f436..6ab8acf4c15 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/InterfaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/NamedTypeSymbolProviders/InterfaceTests.cs @@ -62,6 +62,19 @@ public void VerifyBaseTypeIsNull() Assert.IsNull(namedTypeSymbolProvider.Type.BaseType); } + [Test] + public void IncludesInterfacesInheritedFromBaseType() + { + var baseModel = new DisposableBaseModel(); + var derivedModel = new DerivedModel(baseModel); + var compilation = CompilationHelper.LoadCompilation([baseModel, derivedModel], [typeof(IDisposable)]); + var iNamedSymbol = CompilationHelper.GetSymbol(compilation.Assembly.Modules.First().GlobalNamespace, "DerivedModel"); + + var namedTypeSymbolProvider = new NamedTypeSymbolProvider(iNamedSymbol!, compilation); + + Assert.IsTrue(namedTypeSymbolProvider.Implements.Any(i => i.Equals(typeof(IDisposable)))); + } + private class Model : TypeProvider { protected override string BuildRelativeFilePath() => "."; @@ -90,5 +103,30 @@ protected internal override PropertyProvider[] BuildProperties() ]; } } + + private class DisposableBaseModel : TypeProvider + { + protected override string BuildRelativeFilePath() => "."; + + protected override string BuildName() => "DisposableBaseModel"; + + protected internal override CSharpType[] BuildImplements() + => [typeof(IDisposable)]; + + protected internal override MethodProvider[] BuildMethods() + => [new MethodProvider( + new MethodSignature("Dispose", $"", MethodSignatureModifiers.Public, null, $"", []), + Snippet.ThrowExpression(Snippet.Null), + this)]; + } + + private class DerivedModel(DisposableBaseModel baseModel) : TypeProvider + { + protected override string BuildRelativeFilePath() => "."; + + protected override string BuildName() => "DerivedModel"; + + protected override CSharpType? BuildBaseType() => baseModel.Type; + } } } 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..503c7afcd8b 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 @@ -29,9 +29,9 @@ 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) + private static CSharpType CreateSystemCSharpType(string name, string ns, CSharpType? baseType = null) => new(name, ns, isValueType: false, isNullable: false, declaringType: null, - args: Array.Empty(), isPublic: true, isStruct: false); + args: Array.Empty(), isPublic: true, isStruct: false, baseType: baseType); [SetUp] public void Setup() @@ -98,6 +98,27 @@ public void CanServeAsBaseModelProvider() Assert.IsInstanceOf(derivedProvider.BaseModelProvider); } + [Test] + public void CanRepresentExternalBaseChainWithoutSeparateInheritedProperties() + { + var baseSystemType = CreateSystemCSharpType("ResourceData", "TestFramework"); + var baseInputModel = InputFactory.Model("Resource", properties: []); + var baseProvider = new SystemObjectModelProvider(baseSystemType, baseInputModel); + + var inputModel = InputFactory.Model( + "TrackedResource", + properties: [InputFactory.Property("resourceType", InputPrimitiveType.String, wireName: "type")]); + var systemTypeWithBase = CreateSystemCSharpType("TrackedResourceData", "TestFramework", baseSystemType); + CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseSystemType] = baseProvider; + + var provider = new SystemObjectModelProvider(systemTypeWithBase, inputModel, skipDerivedConstructorParameters: true); + + Assert.AreSame(baseProvider, provider.BaseModelProvider); + Assert.AreEqual(baseProvider.Type, provider.Type.BaseType); + Assert.AreEqual(1, provider.Properties.Count); + Assert.AreEqual("ResourceType", provider.Properties[0].Name); + } + // ------------------------------------------------------------------- // 3. Property deduplication: properties matching framework base are skipped // -------------------------------------------------------------------