diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 2287b15c23d..e8abf90fbe5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -1256,16 +1256,29 @@ protected override ScmMethodProvider[] BuildMethods() protected sealed override IReadOnlyList BuildMethodsForBackCompatibility(IEnumerable originalMethods) { + List materializedMethods = [.. originalMethods]; + if (LastContractView?.Methods == null || LastContractView.Methods.Count == 0) { - return [.. originalMethods]; + return materializedMethods; } - var currentMethodSignatures = BuildCurrentMethodSignatures(originalMethods); + var currentMethodSignatures = BuildCurrentMethodSignatures(materializedMethods); + + ProcessBackCompatForParameterReordering(materializedMethods, currentMethodSignatures); + ProcessBackCompatForNewOptionalParameters(materializedMethods, currentMethodSignatures); + + return materializedMethods; + } + + private void ProcessBackCompatForParameterReordering( + IList materializedMethods, + Dictionary currentMethodSignatures) + { var updatedSignatureToOriginal = new Dictionary(MethodSignature.MethodSignatureComparer); var methodsWithReorderedParams = new List(); - foreach (var previousMethod in LastContractView.Methods) + foreach (var previousMethod in LastContractView!.Methods) { if (!ShouldProcessMethodForBackCompat(previousMethod.Signature, currentMethodSignatures)) { @@ -1290,10 +1303,8 @@ protected sealed override IReadOnlyList BuildMethodsForBackCompa if (methodsWithReorderedParams.Count > 0) { - UpdateConvenienceMethodsForBackCompat(originalMethods, methodsWithReorderedParams, updatedSignatureToOriginal); + UpdateConvenienceMethodsForBackCompat(materializedMethods, methodsWithReorderedParams, updatedSignatureToOriginal); } - - return [.. originalMethods]; } private Dictionary BuildCurrentMethodSignatures(IEnumerable originalMethods) @@ -1748,5 +1759,155 @@ private static void UpdateXmlDocProviderForParamReorder( xmlDocs.Update(parameters: reorderedParamDocs); } } + + private void ProcessBackCompatForNewOptionalParameters( + List methods, + Dictionary currentMethodSignatures) + { + var currentMethodsByName = new Dictionary>(); + foreach (var method in currentMethodSignatures.Values) + { + if (method is ScmMethodProvider { Kind: ScmMethodKind.CreateRequest }) + { + continue; + } + + if (!currentMethodsByName.TryGetValue(method.Signature.Name, out var list)) + { + list = []; + currentMethodsByName[method.Signature.Name] = list; + } + list.Add(method); + } + + foreach (var previousMethod in LastContractView!.Methods) + { + var previousSignature = previousMethod.Signature; + + if (!previousSignature.Modifiers.HasFlag(MethodSignatureModifiers.Public) && + !previousSignature.Modifiers.HasFlag(MethodSignatureModifiers.Protected)) + { + continue; + } + + if (currentMethodSignatures.ContainsKey(previousSignature) || + !currentMethodsByName.TryGetValue(previousSignature.Name, out var candidates)) + { + continue; + } + + ScmMethodProvider? matchedCurrent = null; + foreach (var candidate in candidates) + { + if (candidate is ScmMethodProvider { Kind: ScmMethodKind.Convenience or ScmMethodKind.Protocol } scmCandidate && + HasNewOptionalNonBodyParametersOnly(previousSignature, scmCandidate.Signature)) + { + matchedCurrent = scmCandidate; + break; + } + } + + if (matchedCurrent is null) + { + continue; + } + + var overload = BuildBackCompatOverloadForNewOptionalParameters(previousMethod, matchedCurrent); + if (overload == null || !currentMethodSignatures.TryAdd(overload.Signature, overload)) + { + continue; + } + + methods.Add(overload); + CodeModelGenerator.Instance.Emitter.Debug( + $"Added back-compat overload for '{Name}.{previousSignature.Name}' to handle new optional parameter(s) introduced relative to the last contract.", + BackCompatibilityChangeCategory.SvcMethodNewOptionalParameterOverloadAdded); + } + } + + // Returns true when currentSignature contains all parameters of previousSignature in the same + // relative order, every "extra" parameter is optional, and none of the extras are body parameters. + private static bool HasNewOptionalNonBodyParametersOnly( + MethodSignature previousSignature, + MethodSignature currentSignature) + { + if (currentSignature.Parameters.Count <= previousSignature.Parameters.Count) + { + return false; + } + + if (previousSignature.ReturnType is null + ? currentSignature.ReturnType is not null + : !previousSignature.ReturnType.AreNamesEqual(currentSignature.ReturnType)) + { + return false; + } + + // Walk current parameters and ensure previous parameters appear in the same relative order + // (matched by variable name and type), with every "extra" parameter being optional and non-body. + int previousIndex = 0; + for (int currentIndex = 0; currentIndex < currentSignature.Parameters.Count; currentIndex++) + { + var currentParam = currentSignature.Parameters[currentIndex]; + + if (previousIndex < previousSignature.Parameters.Count) + { + var previousParam = previousSignature.Parameters[previousIndex]; + if (currentParam.Name.ToVariableName() == previousParam.Name.ToVariableName() && + currentParam.Type.AreNamesEqual(previousParam.Type)) + { + previousIndex++; + continue; + } + } + + // Per the Service-Driven Evolution guidance, we only emit a back-compat overload when + // the added parameter is *optional*. A parameter without a default is required, and + // adding a required parameter is itself a breaking change. + if (currentParam.DefaultValue is null) + { + return false; + } + + if (currentParam.Location == ParameterLocation.Body) + { + return false; + } + } + + // All previous parameters must have been matched. + return previousIndex == previousSignature.Parameters.Count; + } + + private ScmMethodProvider? BuildBackCompatOverloadForNewOptionalParameters( + MethodProvider previousMethod, + ScmMethodProvider currentMethod) + { + var previousSignature = previousMethod.Signature; + var currentSignature = currentMethod.Signature; + + var previousParamsByName = new Dictionary(); + foreach (var p in previousSignature.Parameters) + { + previousParamsByName.TryAdd(p.Name, p); + } + + var arguments = new List(currentSignature.Parameters.Count); + foreach (var currentParam in currentSignature.Parameters) + { + ValueExpression value = previousParamsByName.TryGetValue(currentParam.Name, out var prevParam) + ? prevParam + : (currentParam.DefaultValue ?? Default); + arguments.Add(PositionalReference(currentParam.Name, value)); + } + + return new ScmMethodProvider( + signature: MethodSignatureHelper.BuildBackCompatMethodSignature(previousSignature, hideMethod: true), + bodyStatements: Return(This.Invoke(currentSignature.Name, arguments)), + enclosingType: this, + methodKind: currentMethod.Kind, + xmlDocProvider: previousMethod.XmlDocs, + serviceMethod: currentMethod.ServiceMethod); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs index 4af2c5add7f..48275b97be9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs @@ -3149,6 +3149,264 @@ public async Task BackCompatibility_DuplicateMethodSignatureDoesNotThrow() Assert.DoesNotThrow(() => processMethod?.Invoke(clientProvider, null)); } + // Last contract has GetData(int param1, string param2, CancellationToken) (and async). + // The current TypeSpec adds a new optional non-body (header) parameter `param3`. + // Expected: a hidden back-compat overload matching the previous signature is added that + // delegates to the new method, passing default for the new parameter. + [Test] + public async Task BackCompatibility_NewOptionalNonBodyParameterAdded() + { + var param1 = InputFactory.QueryParameter("param1", InputPrimitiveType.Int32, isRequired: true); + var param2 = InputFactory.BodyParameter("param2", InputPrimitiveType.String, isRequired: true); + var param3 = InputFactory.HeaderParameter("param3", InputPrimitiveType.Boolean, isRequired: false); + + var operation = InputFactory.Operation( + "GetData", + parameters: [param1, param2, param3], + responses: [InputFactory.OperationResponse([200], bodytype: InputPrimitiveType.String)]); + + List methodParameters = + [ + InputFactory.MethodParameter("param1", InputPrimitiveType.Int32, location: InputRequestLocation.Query, isRequired: true), + InputFactory.MethodParameter("param2", InputPrimitiveType.String, location: InputRequestLocation.Body, isRequired: true), + InputFactory.MethodParameter("param3", InputPrimitiveType.Boolean, location: InputRequestLocation.Header, isRequired: false), + ]; + + var method = InputFactory.BasicServiceMethod("GetData", operation, parameters: [.. methodParameters]); + var client = InputFactory.Client(TestClientName, methods: [method]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var processMethod = typeof(ClientProvider).GetMethod("ProcessTypeForBackCompatibility", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + processMethod?.Invoke(clientProvider, null); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider(clientProvider!, name => name == "GetData" || name == "GetDataAsync")); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + // The current TypeSpec adds two new optional non-body parameters relative to the last contract. + // Expected: a single back-compat overload matching the previous signature is added that + // delegates to the new method, passing default for both new parameters. + [Test] + public async Task BackCompatibility_MultipleNewOptionalNonBodyParametersAdded() + { + var param1 = InputFactory.QueryParameter("param1", InputPrimitiveType.Int32, isRequired: true); + var param2 = InputFactory.BodyParameter("param2", InputPrimitiveType.String, isRequired: true); + var param3 = InputFactory.HeaderParameter("param3", InputPrimitiveType.Boolean, isRequired: false); + var param4 = InputFactory.QueryParameter("param4", InputPrimitiveType.String, isRequired: false); + + var operation = InputFactory.Operation( + "GetData", + parameters: [param1, param2, param3, param4], + responses: [InputFactory.OperationResponse([200], bodytype: InputPrimitiveType.String)]); + + List methodParameters = + [ + InputFactory.MethodParameter("param1", InputPrimitiveType.Int32, location: InputRequestLocation.Query, isRequired: true), + InputFactory.MethodParameter("param2", InputPrimitiveType.String, location: InputRequestLocation.Body, isRequired: true), + InputFactory.MethodParameter("param3", InputPrimitiveType.Boolean, location: InputRequestLocation.Header, isRequired: false), + InputFactory.MethodParameter("param4", InputPrimitiveType.String, location: InputRequestLocation.Query, isRequired: false), + ]; + + var method = InputFactory.BasicServiceMethod("GetData", operation, parameters: [.. methodParameters]); + var client = InputFactory.Client(TestClientName, methods: [method]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var processMethod = typeof(ClientProvider).GetMethod("ProcessTypeForBackCompatibility", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + processMethod?.Invoke(clientProvider, null); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider(clientProvider!, name => name == "GetData" || name == "GetDataAsync")); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + // Last contract has GetData(int param1, ModelType body, CancellationToken) (and async). + // The current TypeSpec adds a new optional non-body (header) parameter `param3`. + // Expected: a hidden back-compat overload matching the previous signature is added even when + // the method has a model-typed request body. + [Test] + public async Task BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody() + { + var bodyModel = InputFactory.Model( + "SampleModel", + properties: [InputFactory.Property("name", InputPrimitiveType.String, isRequired: true)]); + + var param1 = InputFactory.QueryParameter("param1", InputPrimitiveType.Int32, isRequired: true); + var param2 = InputFactory.BodyParameter("body", bodyModel, isRequired: true); + var param3 = InputFactory.HeaderParameter("param3", InputPrimitiveType.Boolean, isRequired: false); + + var operation = InputFactory.Operation( + "GetData", + parameters: [param1, param2, param3], + responses: [InputFactory.OperationResponse([200], bodytype: InputPrimitiveType.String)]); + + List methodParameters = + [ + InputFactory.MethodParameter("param1", InputPrimitiveType.Int32, location: InputRequestLocation.Query, isRequired: true), + InputFactory.MethodParameter("body", bodyModel, location: InputRequestLocation.Body, isRequired: true), + InputFactory.MethodParameter("param3", InputPrimitiveType.Boolean, location: InputRequestLocation.Header, isRequired: false), + ]; + + var method = InputFactory.BasicServiceMethod("GetData", operation, parameters: [.. methodParameters]); + var client = InputFactory.Client(TestClientName, methods: [method]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var processMethod = typeof(ClientProvider).GetMethod("ProcessTypeForBackCompatibility", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + processMethod?.Invoke(clientProvider, null); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider(clientProvider!, name => name == "GetData" || name == "GetDataAsync")); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + // When the new parameter is a body parameter, the back-compat overload should NOT be added. + // See https://github.com/Azure/azure-sdk-for-net/blob/main/doc/DataPlaneCodeGeneration/ServiceDrivenEvolution.md#a-method-gets-a-new-optional-parameter + [Test] + public async Task BackCompatibility_NewOptionalBodyParameterDoesNotAddBackCompatOverload() + { + // Last contract had a GetData(int param2, CancellationToken) method (no body parameter). + // Current method adds an optional body parameter `param1`. + var param1 = InputFactory.BodyParameter("param1", InputPrimitiveType.String, isRequired: false); + var param2 = InputFactory.QueryParameter("param2", InputPrimitiveType.Int32, isRequired: true); + + var operation = InputFactory.Operation( + "GetData", + parameters: [param1, param2], + responses: [InputFactory.OperationResponse([200], bodytype: InputPrimitiveType.String)]); + + List methodParameters = + [ + InputFactory.MethodParameter("param1", InputPrimitiveType.String, location: InputRequestLocation.Body, isRequired: false), + InputFactory.MethodParameter("param2", InputPrimitiveType.Int32, location: InputRequestLocation.Query, isRequired: true), + ]; + + var method = InputFactory.BasicServiceMethod("GetData", operation, parameters: [.. methodParameters]); + var client = InputFactory.Client(TestClientName, methods: [method]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var processMethod = typeof(ClientProvider).GetMethod("ProcessTypeForBackCompatibility", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + processMethod?.Invoke(clientProvider, null); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider(clientProvider!, name => name == "GetData" || name == "GetDataAsync")); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + // When the new parameter is required (not optional), the back-compat overload should NOT be added, + // even if it is non-body. Adding such an overload would require us to invent a value for a required + // parameter, which would be unsafe. + [Test] + public async Task BackCompatibility_NewRequiredParameterDoesNotAddBackCompatOverload() + { + var param1 = InputFactory.BodyParameter("param1", InputPrimitiveType.String, isRequired: true); + var param2 = InputFactory.QueryParameter("param2", InputPrimitiveType.Int32, isRequired: true); + var param3 = InputFactory.HeaderParameter("param3", InputPrimitiveType.Boolean, isRequired: true); + + var operation = InputFactory.Operation( + "GetData", + parameters: [param1, param2, param3], + responses: [InputFactory.OperationResponse([200], bodytype: InputPrimitiveType.String)]); + + List methodParameters = + [ + InputFactory.MethodParameter("param1", InputPrimitiveType.String, location: InputRequestLocation.Body, isRequired: true), + InputFactory.MethodParameter("param2", InputPrimitiveType.Int32, location: InputRequestLocation.Query, isRequired: true), + InputFactory.MethodParameter("param3", InputPrimitiveType.Boolean, location: InputRequestLocation.Header, isRequired: true), + ]; + + var method = InputFactory.BasicServiceMethod("GetData", operation, parameters: [.. methodParameters]); + var client = InputFactory.Client(TestClientName, methods: [method]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var processMethod = typeof(ClientProvider).GetMethod("ProcessTypeForBackCompatibility", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + processMethod?.Invoke(clientProvider, null); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider(clientProvider!, name => name == "GetData" || name == "GetDataAsync")); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + // Last contract has GetData(string itemId, int filter, CancellationToken) (and async) where + // itemId is a path parameter, filter is a required query, and a required header is present. + // The current TypeSpec adds a new optional query parameter `sort`. + // Expected: a hidden back-compat overload matching the previous signature is added even when + // the operation mixes path, query, and header parameters. + [Test] + public async Task BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters() + { + var itemId = InputFactory.PathParameter("itemId", InputPrimitiveType.String, isRequired: true); + var filter = InputFactory.QueryParameter("filter", InputPrimitiveType.Int32, isRequired: true); + var region = InputFactory.HeaderParameter("region", InputPrimitiveType.String, isRequired: true); + var sort = InputFactory.QueryParameter("sort", InputPrimitiveType.String, isRequired: false); + + var operation = InputFactory.Operation( + "GetData", + path: "/items/{itemId}", + parameters: [itemId, filter, region, sort], + responses: [InputFactory.OperationResponse([200], bodytype: InputPrimitiveType.String)]); + + List methodParameters = + [ + InputFactory.MethodParameter("itemId", InputPrimitiveType.String, location: InputRequestLocation.Path, isRequired: true), + InputFactory.MethodParameter("filter", InputPrimitiveType.Int32, location: InputRequestLocation.Query, isRequired: true), + InputFactory.MethodParameter("region", InputPrimitiveType.String, location: InputRequestLocation.Header, isRequired: true), + InputFactory.MethodParameter("sort", InputPrimitiveType.String, location: InputRequestLocation.Query, isRequired: false), + ]; + + var method = InputFactory.BasicServiceMethod("GetData", operation, parameters: [.. methodParameters]); + var client = InputFactory.Client(TestClientName, methods: [method]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + Assert.IsNotNull(clientProvider!.LastContractView); + + var processMethod = typeof(ClientProvider).GetMethod("ProcessTypeForBackCompatibility", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + processMethod?.Invoke(clientProvider, null); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider(clientProvider!, name => name == "GetData" || name == "GetDataAsync")); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + [Test] public void ServerTemplateWithBasePathOnly_DoesNotDuplicateBasePath() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_MultipleNewOptionalNonBodyParametersAdded.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_MultipleNewOptionalNonBodyParametersAdded.cs new file mode 100644 index 00000000000..f1caf7a33e4 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_MultipleNewOptionalNonBodyParametersAdded.cs @@ -0,0 +1,62 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual global::System.ClientModel.ClientResult GetData(int param1, global::System.ClientModel.BinaryContent content, bool? param3 = default, string param4 = default, global::System.ClientModel.Primitives.RequestOptions options = null) + { + global::Sample.Argument.AssertNotNull(content, nameof(content)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param1, content, param3, param4, options); + return global::System.ClientModel.ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); + } + + public virtual async global::System.Threading.Tasks.Task GetDataAsync(int param1, global::System.ClientModel.BinaryContent content, bool? param3 = default, string param4 = default, global::System.ClientModel.Primitives.RequestOptions options = null) + { + global::Sample.Argument.AssertNotNull(content, nameof(content)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param1, content, param3, param4, options); + return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + public virtual global::System.ClientModel.ClientResult GetData(int param1, string param2, bool? param3 = default, string param4 = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(param2, nameof(param2)); + + using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param2)); + global::System.ClientModel.ClientResult result = this.GetData(param1, content, param3, param4, cancellationToken.ToRequestOptions()); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + public virtual async global::System.Threading.Tasks.Task> GetDataAsync(int param1, string param2, bool? param3 = default, string param4 = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(param2, nameof(param2)); + + using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param2)); + global::System.ClientModel.ClientResult result = await this.GetDataAsync(param1, content, param3, param4, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public virtual global::System.ClientModel.ClientResult GetData(int param1, string param2, global::System.Threading.CancellationToken cancellationToken) + { + return this.GetData(param1: param1, param2: param2, param3: default, param4: default, cancellationToken: cancellationToken); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public virtual global::System.Threading.Tasks.Task> GetDataAsync(int param1, string param2, global::System.Threading.CancellationToken cancellationToken) + { + return this.GetDataAsync(param1: param1, param2: param2, param3: default, param4: default, cancellationToken: cancellationToken); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_MultipleNewOptionalNonBodyParametersAdded/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_MultipleNewOptionalNonBodyParametersAdded/TestClient.cs new file mode 100644 index 00000000000..5bca61e2cea --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_MultipleNewOptionalNonBodyParametersAdded/TestClient.cs @@ -0,0 +1,21 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual ClientResult GetData(int param1, string param2, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public virtual Task> GetDataAsync(int param1, string param2, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalBodyParameterDoesNotAddBackCompatOverload.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalBodyParameterDoesNotAddBackCompatOverload.cs new file mode 100644 index 00000000000..bc72ecb5894 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalBodyParameterDoesNotAddBackCompatOverload.cs @@ -0,0 +1,41 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual global::System.ClientModel.ClientResult GetData(int param2, global::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options = null) + { + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param2, content, options); + return global::System.ClientModel.ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); + } + + public virtual async global::System.Threading.Tasks.Task GetDataAsync(int param2, global::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options = null) + { + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param2, content, options); + return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + public virtual global::System.ClientModel.ClientResult GetData(int param2, string param1 = default, global::System.Threading.CancellationToken cancellationToken = default) + { + using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param1)); + global::System.ClientModel.ClientResult result = this.GetData(param2, content, cancellationToken.ToRequestOptions()); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + public virtual async global::System.Threading.Tasks.Task> GetDataAsync(int param2, string param1 = default, global::System.Threading.CancellationToken cancellationToken = default) + { + using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param1)); + global::System.ClientModel.ClientResult result = await this.GetDataAsync(param2, content, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalBodyParameterDoesNotAddBackCompatOverload/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalBodyParameterDoesNotAddBackCompatOverload/TestClient.cs new file mode 100644 index 00000000000..0b21ee524da --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalBodyParameterDoesNotAddBackCompatOverload/TestClient.cs @@ -0,0 +1,21 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual ClientResult GetData(int param2, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public virtual Task> GetDataAsync(int param2, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAdded.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAdded.cs new file mode 100644 index 00000000000..3050a4a5b27 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAdded.cs @@ -0,0 +1,62 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual global::System.ClientModel.ClientResult GetData(int param1, global::System.ClientModel.BinaryContent content, bool? param3 = default, global::System.ClientModel.Primitives.RequestOptions options = null) + { + global::Sample.Argument.AssertNotNull(content, nameof(content)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param1, content, param3, options); + return global::System.ClientModel.ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); + } + + public virtual async global::System.Threading.Tasks.Task GetDataAsync(int param1, global::System.ClientModel.BinaryContent content, bool? param3 = default, global::System.ClientModel.Primitives.RequestOptions options = null) + { + global::Sample.Argument.AssertNotNull(content, nameof(content)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param1, content, param3, options); + return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + public virtual global::System.ClientModel.ClientResult GetData(int param1, string param2, bool? param3 = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(param2, nameof(param2)); + + using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param2)); + global::System.ClientModel.ClientResult result = this.GetData(param1, content, param3, cancellationToken.ToRequestOptions()); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + public virtual async global::System.Threading.Tasks.Task> GetDataAsync(int param1, string param2, bool? param3 = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(param2, nameof(param2)); + + using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param2)); + global::System.ClientModel.ClientResult result = await this.GetDataAsync(param1, content, param3, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public virtual global::System.ClientModel.ClientResult GetData(int param1, string param2, global::System.Threading.CancellationToken cancellationToken) + { + return this.GetData(param1: param1, param2: param2, param3: default, cancellationToken: cancellationToken); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public virtual global::System.Threading.Tasks.Task> GetDataAsync(int param1, string param2, global::System.Threading.CancellationToken cancellationToken) + { + return this.GetDataAsync(param1: param1, param2: param2, param3: default, cancellationToken: cancellationToken); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAdded/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAdded/TestClient.cs new file mode 100644 index 00000000000..5bca61e2cea --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAdded/TestClient.cs @@ -0,0 +1,21 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual ClientResult GetData(int param1, string param2, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public virtual Task> GetDataAsync(int param1, string param2, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody.cs new file mode 100644 index 00000000000..7abd57c6eeb --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody.cs @@ -0,0 +1,60 @@ +// + +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Sample.Models; + +namespace Sample +{ + public partial class TestClient + { + public virtual global::System.ClientModel.ClientResult GetData(int param1, global::System.ClientModel.BinaryContent content, bool? param3 = default, global::System.ClientModel.Primitives.RequestOptions options = null) + { + global::Sample.Argument.AssertNotNull(content, nameof(content)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param1, content, param3, options); + return global::System.ClientModel.ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); + } + + public virtual async global::System.Threading.Tasks.Task GetDataAsync(int param1, global::System.ClientModel.BinaryContent content, bool? param3 = default, global::System.ClientModel.Primitives.RequestOptions options = null) + { + global::Sample.Argument.AssertNotNull(content, nameof(content)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param1, content, param3, options); + return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + public virtual global::System.ClientModel.ClientResult GetData(int param1, global::Sample.Models.SampleModel body, bool? param3 = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNull(body, nameof(body)); + + global::System.ClientModel.ClientResult result = this.GetData(param1, body, param3, cancellationToken.ToRequestOptions()); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + public virtual async global::System.Threading.Tasks.Task> GetDataAsync(int param1, global::Sample.Models.SampleModel body, bool? param3 = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNull(body, nameof(body)); + + global::System.ClientModel.ClientResult result = await this.GetDataAsync(param1, body, param3, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public virtual global::System.ClientModel.ClientResult GetData(int param1, global::Sample.Models.SampleModel body, global::System.Threading.CancellationToken cancellationToken) + { + return this.GetData(param1: param1, body: body, param3: default, cancellationToken: cancellationToken); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public virtual global::System.Threading.Tasks.Task> GetDataAsync(int param1, global::Sample.Models.SampleModel body, global::System.Threading.CancellationToken cancellationToken) + { + return this.GetDataAsync(param1: param1, body: body, param3: default, cancellationToken: cancellationToken); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody/TestClient.cs new file mode 100644 index 00000000000..d4332ecd679 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody/TestClient.cs @@ -0,0 +1,21 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual ClientResult GetData(int param1, global::Sample.Models.SampleModel body, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public virtual Task> GetDataAsync(int param1, global::Sample.Models.SampleModel body, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters.cs new file mode 100644 index 00000000000..ecd5a27c74a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters.cs @@ -0,0 +1,63 @@ +// + +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual global::System.ClientModel.ClientResult GetData(string itemId, int filter, string region, string sort, global::System.ClientModel.Primitives.RequestOptions options) + { + global::Sample.Argument.AssertNotNullOrEmpty(itemId, nameof(itemId)); + global::Sample.Argument.AssertNotNullOrEmpty(region, nameof(region)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(itemId, filter, region, sort, options); + return global::System.ClientModel.ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); + } + + public virtual async global::System.Threading.Tasks.Task GetDataAsync(string itemId, int filter, string region, string sort, global::System.ClientModel.Primitives.RequestOptions options) + { + global::Sample.Argument.AssertNotNullOrEmpty(itemId, nameof(itemId)); + global::Sample.Argument.AssertNotNullOrEmpty(region, nameof(region)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(itemId, filter, region, sort, options); + return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + public virtual global::System.ClientModel.ClientResult GetData(string itemId, int filter, string region, string sort = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(itemId, nameof(itemId)); + global::Sample.Argument.AssertNotNullOrEmpty(region, nameof(region)); + + global::System.ClientModel.ClientResult result = this.GetData(itemId, filter, region, sort, cancellationToken.ToRequestOptions()); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + public virtual async global::System.Threading.Tasks.Task> GetDataAsync(string itemId, int filter, string region, string sort = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(itemId, nameof(itemId)); + global::Sample.Argument.AssertNotNullOrEmpty(region, nameof(region)); + + global::System.ClientModel.ClientResult result = await this.GetDataAsync(itemId, filter, region, sort, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public virtual global::System.ClientModel.ClientResult GetData(string itemId, int filter, string region, global::System.Threading.CancellationToken cancellationToken) + { + return this.GetData(itemId: itemId, filter: filter, region: region, sort: default, cancellationToken: cancellationToken); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public virtual global::System.Threading.Tasks.Task> GetDataAsync(string itemId, int filter, string region, global::System.Threading.CancellationToken cancellationToken) + { + return this.GetDataAsync(itemId: itemId, filter: filter, region: region, sort: default, cancellationToken: cancellationToken); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters/TestClient.cs new file mode 100644 index 00000000000..461df2c8993 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters/TestClient.cs @@ -0,0 +1,21 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual ClientResult GetData(string itemId, int filter, string region, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public virtual Task> GetDataAsync(string itemId, int filter, string region, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewRequiredParameterDoesNotAddBackCompatOverload.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewRequiredParameterDoesNotAddBackCompatOverload.cs new file mode 100644 index 00000000000..df446a2081b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewRequiredParameterDoesNotAddBackCompatOverload.cs @@ -0,0 +1,49 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual global::System.ClientModel.ClientResult GetData(int param2, bool param3, global::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options = null) + { + global::Sample.Argument.AssertNotNull(content, nameof(content)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param2, param3, content, options); + return global::System.ClientModel.ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); + } + + public virtual async global::System.Threading.Tasks.Task GetDataAsync(int param2, bool param3, global::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options = null) + { + global::Sample.Argument.AssertNotNull(content, nameof(content)); + + using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateGetDataRequest(param2, param3, content, options); + return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + public virtual global::System.ClientModel.ClientResult GetData(int param2, bool param3, string param1, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(param1, nameof(param1)); + + using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param1)); + global::System.ClientModel.ClientResult result = this.GetData(param2, param3, content, cancellationToken.ToRequestOptions()); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + + public virtual async global::System.Threading.Tasks.Task> GetDataAsync(int param2, bool param3, string param1, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(param1, nameof(param1)); + + using global::System.ClientModel.BinaryContent content = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(param1)); + global::System.ClientModel.ClientResult result = await this.GetDataAsync(param2, param3, content, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + return global::System.ClientModel.ClientResult.FromValue(result.GetRawResponse().Content.ToObjectFromJson(), result.GetRawResponse()); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewRequiredParameterDoesNotAddBackCompatOverload/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewRequiredParameterDoesNotAddBackCompatOverload/TestClient.cs new file mode 100644 index 00000000000..18ff51338f3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewRequiredParameterDoesNotAddBackCompatOverload/TestClient.cs @@ -0,0 +1,21 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual ClientResult GetData(int param2, string param1, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public virtual Task> GetDataAsync(int param2, string param1, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs index dbd213f608f..5b635fe8b9a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs @@ -39,5 +39,8 @@ public enum BackCompatibilityChangeCategory /// A back-compat model factory method could not be created and was skipped. ModelFactoryMethodSkipped, + + /// A back-compat overload of a client method was added because new optional non-body parameter(s) were introduced relative to the last contract. + SvcMethodNewOptionalParameterOverloadAdded, } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs index 7fed8e03981..9b83da66b33 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs @@ -180,6 +180,7 @@ public void WriteBufferedMessages() BackCompatibilityChangeCategory.ModelFactoryMethodReplaced => "Model Factory Method Replaced For Back-Compat", BackCompatibilityChangeCategory.ModelFactoryMethodAdded => "Model Factory Method Added For Back-Compat", BackCompatibilityChangeCategory.ModelFactoryMethodSkipped => "Model Factory Method Back-Compat Skipped", + BackCompatibilityChangeCategory.SvcMethodNewOptionalParameterOverloadAdded => "Method Back-Compat Overload Added For New Optional Parameter", _ => category.ToString(), }; diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 3dda7eceeed..58033ebdc03 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -21,6 +21,8 @@ - [Method Parameter Name Preserved from Last Contract](#scenario-method-parameter-name-preserved-from-last-contract) - [Content-Type Parameter Ordering](#content-type-parameter-ordering) - [Content-Type Before Body Preserved from Last Contract](#scenario-content-type-before-body-preserved-from-last-contract) + - [Client Methods](#client-methods) + - [New Optional Non-Body Parameter Added to a Service Method](#scenario-new-optional-non-body-parameter-added-to-a-service-method) ## Overview @@ -784,3 +786,65 @@ public virtual ClientResult UpdateSkillDefaultVersion(string skillId, string con // contentType stays before content for backward compatibility } ``` + +### Client Methods + +#### Scenario: New Optional Non-Body Parameter Added to a Service Method + +**Description:** When the current TypeSpec adds one or more new optional non-body parameters (e.g. query, header, path) to an existing service method, the generator emits a hidden back-compat overload that matches the previous contract's signature and delegates to the new method, passing `default` for the new parameter(s). The behavior is **intentionally restricted to non-body parameters** because adding a body parameter typically reflects a schema change and is handled differently. + +**Rules:** + +- The previous method's parameters must appear, in the same relative order and matching by name and type, within the current method's parameters. +- Every "extra" parameter on the current method must be optional (i.e. have a default value). +- No "extra" parameter on the current method may be a body parameter; if any extra parameter is a body parameter, no back-compat overload is added. +- The back-compat overload is hidden via `[EditorBrowsable(EditorBrowsableState.Never)]` and has all default values stripped from its parameters to avoid ambiguous call sites. + +**Example:** + +Previous version of the client: + +```csharp +public virtual ClientResult GetData(int p1, BinaryContent body, RequestOptions options = null); +public virtual Task GetDataAsync(int p1, BinaryContent body, RequestOptions options = null); +``` + +Current TypeSpec adds an optional query parameter `p2`: + +```typespec +op getData(@query p1: int32, @body body: SampleModel, @query p2?: boolean): string; +``` + +**Generated Client:** + +The generated client includes the current methods (with the new optional `p2` parameter) and the hidden back-compat overloads that match the previous contract's signature. Required parameters come first, followed by optional parameters, with `RequestOptions` last — matching the [Azure SDK parameter ordering guidelines](https://azure.github.io/azure-sdk/dotnet_implementation.html#parameter-presence-and-ordering). + +```csharp +// Current sync method generated from the updated TypeSpec. +public virtual ClientResult GetData(int p1, BinaryContent body, bool? p2 = default, RequestOptions options = null) +{ + // ... implementation ... +} + +// Current async method generated from the updated TypeSpec. +public virtual async Task GetDataAsync(int p1, BinaryContent body, bool? p2 = default, RequestOptions options = null) +{ + // ... implementation ... +} + +// Back-compat sync overload matching the previous contract's signature. +[EditorBrowsable(EditorBrowsableState.Never)] +public virtual ClientResult GetData(int p1, BinaryContent body, RequestOptions options) +{ + return this.GetData(p1: p1, body: body, p2: default, options: options); +} + +// Back-compat async overload matching the previous contract's signature. +[EditorBrowsable(EditorBrowsableState.Never)] +public virtual Task GetDataAsync(int p1, BinaryContent body, RequestOptions options) +{ + return this.GetDataAsync(p1: p1, body: body, p2: default, options: options); +} +``` + +The back-compat overloads are hidden from IntelliSense via `[EditorBrowsable(EditorBrowsableState.Never)]`, have all default values stripped to avoid ambiguous call sites with the current methods, and delegate to the current method passing `default` for each new parameter.