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 590d2d4b387..a591ee63581 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 @@ -123,7 +123,7 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m protected override TypeSignatureModifiers BuildDeclarationModifiers() => _model.DeclarationModifiers; protected override IReadOnlyList BuildMethodsForBackCompatibility(IEnumerable originalMethods) - => [.. originalMethods]; + => base.BuildMethodsForBackCompatibility(originalMethods); protected override IReadOnlyList BuildConstructorsForBackCompatibility(IEnumerable originalConstructors) => [.. originalConstructors]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/MrwSerializationTypeDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/MrwSerializationTypeDefinitionTests.cs index 32ade7fd931..c830ae3ff6d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/MrwSerializationTypeDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/MrwSerializationTypeDefinitionTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.ClientModel.Providers; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; @@ -14,6 +15,7 @@ using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; +using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Statements; using Microsoft.TypeSpec.Generator.Tests.Common; using NUnit.Framework; @@ -804,6 +806,49 @@ public void TestBuildExplicitFromClientResult() Assert.IsNotNull(methodBody); } + [Test] + public async Task ExplicitClientResultOperatorPreservesLastContractParameterName() + { + var inputModel = InputFactory.Model("mockInputModel"); + var generator = MockHelpers.LoadMockGenerator( + inputModels: () => [inputModel], + clientResponseApi: new RenamedClientResultProvider(ValueExpression.Empty)); + generator.Object.SourceInputModel = new SourceInputModel( + null, + await Helpers.GetCompilationFromDirectoryAsync()); + generator.Object.TypeFactory.RootOutputModels.Add(inputModel); + var model = generator.Object.OutputLibrary.TypeProviders.OfType().Single(); + + model.EnsureBuilt(); + CSharpGen.ProcessTypeProvidersForBackCompatibility(generator.Object.OutputLibrary); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider( + model.SerializationProviders.Single(), + name => name == model.Name)); + + Assert.AreEqual(Helpers.GetExpectedFromFile(), writer.Write().Content); + } + + [Test] + public void ExplicitClientResultOperatorKeepsCurrentParameterNameWithoutLastContract() + { + var inputModel = InputFactory.Model("mockInputModel"); + var generator = MockHelpers.LoadMockGenerator( + inputModels: () => [inputModel], + clientResponseApi: new RenamedClientResultProvider(ValueExpression.Empty)); + generator.Object.TypeFactory.RootOutputModels.Add(inputModel); + var model = generator.Object.OutputLibrary.TypeProviders.OfType().Single(); + + model.EnsureBuilt(); + CSharpGen.ProcessTypeProvidersForBackCompatibility(generator.Object.OutputLibrary); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider( + model.SerializationProviders.Single(), + name => name == model.Name)); + + Assert.AreEqual(Helpers.GetExpectedFromFile(), writer.Write().Content); + } + [Test] public void TestExplicitFromClientResultNotGeneratedForNonRootOutputModel() { @@ -818,6 +863,12 @@ public void TestExplicitFromClientResultNotGeneratedForNonRootOutputModel() Assert.IsNull(method); } + private record RenamedClientResultProvider(ValueExpression Original) + : ClientResultProvider(Original) + { + public override string ResponseParameterName => "response"; + } + [TestCase(true)] [TestCase(false)] public void TestReadOnlyPropertiesHaveWireCheck(bool isRequired) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/MrwSerializationTypeDefinitionTests/ExplicitClientResultOperatorKeepsCurrentParameterNameWithoutLastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/MrwSerializationTypeDefinitionTests/ExplicitClientResultOperatorKeepsCurrentParameterNameWithoutLastContract.cs new file mode 100644 index 00000000000..d66342e3da8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/MrwSerializationTypeDefinitionTests/ExplicitClientResultOperatorKeepsCurrentParameterNameWithoutLastContract.cs @@ -0,0 +1,21 @@ +// + +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Text.Json; +using Sample.Models; + +namespace Sample +{ + public partial class MockInputModel + { + public static explicit operator MockInputModel(global::System.ClientModel.ClientResult response) + { + global::System.ClientModel.Primitives.PipelineResponse response0 = response.GetRawResponse(); + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(response0.Content, global::Sample.ModelSerializationExtensions.JsonDocumentOptions); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, global::Sample.ModelSerializationExtensions.WireOptions); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/MrwSerializationTypeDefinitionTests/ExplicitClientResultOperatorPreservesLastContractParameterName.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/MrwSerializationTypeDefinitionTests/ExplicitClientResultOperatorPreservesLastContractParameterName.cs new file mode 100644 index 00000000000..f8fb7af87a5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/MrwSerializationTypeDefinitionTests/ExplicitClientResultOperatorPreservesLastContractParameterName.cs @@ -0,0 +1,21 @@ +// + +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Text.Json; +using Sample.Models; + +namespace Sample +{ + public partial class MockInputModel + { + public static explicit operator MockInputModel(global::System.ClientModel.ClientResult result) + { + global::System.ClientModel.Primitives.PipelineResponse response = result.GetRawResponse(); + using global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(response.Content, global::Sample.ModelSerializationExtensions.JsonDocumentOptions); + return global::Sample.Models.MockInputModel.DeserializeMockInputModel(document.RootElement, global::Sample.ModelSerializationExtensions.WireOptions); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/MrwSerializationTypeDefinitionTests/ExplicitClientResultOperatorPreservesLastContractParameterName/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/MrwSerializationTypeDefinitionTests/ExplicitClientResultOperatorPreservesLastContractParameterName/MockInputModel.cs new file mode 100644 index 00000000000..bd38edca86c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/MrwSerializationTypeDefinitionTests/ExplicitClientResultOperatorPreservesLastContractParameterName/MockInputModel.cs @@ -0,0 +1,9 @@ +using System.ClientModel; + +namespace Sample.Models +{ + public partial class MockInputModel + { + public static explicit operator MockInputModel(ClientResult result) => throw null; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs index 94abf17e68e..a6de059ddd2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs @@ -86,11 +86,8 @@ await GeneratedCodeWorkspace.LoadBaselineContract(), LoggingHelpers.LogElapsedTime("All visitors have been applied"); - foreach (var outputType in output.TypeProviders) - { - // Ensure back-compatibility processing is done after all visitors have run - outputType.ProcessTypeForBackCompatibility(); - } + // Ensure back-compatibility processing is done after all visitors have run + ProcessTypeProvidersForBackCompatibility(output); try { @@ -211,6 +208,35 @@ private static void FilterCustomizedMembers(TypeProvider typeProvider) typeProvider.Fields); } + internal static void ProcessTypeProvidersForBackCompatibility(OutputLibrary output) + { + var visited = new HashSet(); + foreach (var typeProvider in output.TypeProviders) + { + ProcessTypeForBackCompatibility(typeProvider, visited); + } + } + + private static void ProcessTypeForBackCompatibility(TypeProvider typeProvider, HashSet visited) + { + if (!visited.Add(typeProvider)) + { + return; + } + + typeProvider.ProcessTypeForBackCompatibility(); + + foreach (var serializationProvider in typeProvider.SerializationProviders) + { + ProcessTypeForBackCompatibility(serializationProvider, visited); + } + + foreach (var nestedType in typeProvider.NestedTypes) + { + ProcessTypeForBackCompatibility(nestedType, visited); + } + } + /// /// Clears the output directory specified by . If is not null, /// the specified files in the output directory will not be deleted. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresImplicitOperatorParameterNameOnSerializationProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresImplicitOperatorParameterNameOnSerializationProvider.cs new file mode 100644 index 00000000000..8835a4b5df4 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresImplicitOperatorParameterNameOnSerializationProvider.cs @@ -0,0 +1,14 @@ +// + +#nullable disable + +namespace Test +{ + public partial class SerializationImplicitBackCompatType + { + public static implicit operator string(global::Test.SerializationImplicitBackCompatType published) + { + return null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresImplicitOperatorParameterNameOnSerializationProvider/SerializationImplicitBackCompatType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresImplicitOperatorParameterNameOnSerializationProvider/SerializationImplicitBackCompatType.cs new file mode 100644 index 00000000000..bd888c79c2c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresImplicitOperatorParameterNameOnSerializationProvider/SerializationImplicitBackCompatType.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public partial class SerializationImplicitBackCompatType + { + public static implicit operator string(SerializationImplicitBackCompatType published) => null; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresOperatorParameterNameOnSerializationProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresOperatorParameterNameOnSerializationProvider.cs new file mode 100644 index 00000000000..37a44a37e3f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresOperatorParameterNameOnSerializationProvider.cs @@ -0,0 +1,14 @@ +// + +#nullable disable + +namespace Test +{ + public partial class SerializationBackCompatType + { + public static explicit operator SerializationBackCompatType(string result) + { + return null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresOperatorParameterNameOnSerializationProvider/SerializationBackCompatType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresOperatorParameterNameOnSerializationProvider/SerializationBackCompatType.cs new file mode 100644 index 00000000000..f98c65754f8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/BuildMethodsForBackCompatibilityRestoresOperatorParameterNameOnSerializationProvider/SerializationBackCompatType.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public partial class SerializationBackCompatType + { + public static explicit operator SerializationBackCompatType(string result) => null; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index b4b6d14584d..975d0bfccfe 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -12,6 +12,7 @@ using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; using Microsoft.TypeSpec.Generator.Tests.Common; +using Microsoft.TypeSpec.Generator.Tests.TestHelpers; using Microsoft.TypeSpec.Generator.Utilities; using NUnit.Framework; @@ -968,6 +969,64 @@ public async Task BuildMethodsForBackCompatibilityKeepsUnpublishedParameterName( Assert.AreEqual(Helpers.GetExpectedFromFile(), actual); } + [Test] + public async Task BuildMethodsForBackCompatibilityRestoresOperatorParameterNameOnSerializationProvider() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var owner = new TestTypeProvider(name: "SerializationBackCompatType", ns: "Test"); + var serializationProvider = new TestTypeProvider(name: "SerializationBackCompatType", ns: "Test"); + var response = new ParameterProvider("response", $"", new CSharpType(typeof(string))); + var explicitOperator = new MethodProvider( + new MethodSignature( + serializationProvider.Name, + $"", + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Explicit | MethodSignatureModifiers.Operator, + serializationProvider.Type, + $"", + [response]), + Snippet.Return(Snippet.Null), + serializationProvider); + + serializationProvider.Update(methods: [explicitOperator]); + owner.Update(serializations: [serializationProvider]); + + var outputLibrary = new TestOutputLibrary(owner); + CSharpGen.ProcessTypeProvidersForBackCompatibility(outputLibrary); + + var actual = new TypeProviderWriter(serializationProvider).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), actual); + } + + [Test] + public async Task BuildMethodsForBackCompatibilityRestoresImplicitOperatorParameterNameOnSerializationProvider() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var owner = new TestTypeProvider(name: "SerializationImplicitBackCompatType", ns: "Test"); + var serializationProvider = new TestTypeProvider(name: "SerializationImplicitBackCompatType", ns: "Test"); + var current = new ParameterProvider("current", $"", serializationProvider.Type); + var implicitOperator = new MethodProvider( + new MethodSignature( + "BinaryContent", + $"", + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Implicit | MethodSignatureModifiers.Operator, + new CSharpType(typeof(string)), + $"", + [current]), + Snippet.Return(Snippet.Null), + serializationProvider); + + serializationProvider.Update(methods: [implicitOperator]); + owner.Update(serializations: [serializationProvider]); + + var outputLibrary = new TestOutputLibrary(owner); + CSharpGen.ProcessTypeProvidersForBackCompatibility(outputLibrary); + + var actual = new TypeProviderWriter(serializationProvider).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), actual); + } + // Validates the positional fallback: when a spec parameter's previously-published name matches // neither its current name nor its spec original name (e.g. a rename by a different generator), // it is restored from the last-contract method that matches by signature (name and parameter types).