From 1a98b170e6702646507aff089783c747606258ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 19:38:25 +0000 Subject: [PATCH 1/9] Initial plan From 60acb0af30d1eb7cf24ce7614f72e15b6cda2f01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 19:49:13 +0000 Subject: [PATCH 2/9] Fix back-compat svc method overloads: handle reserved param names and preserve optional CancellationToken Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/09ec7e5f-2d2f-4a58-8662-7d148879bb09 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- ...nd-cancellation-token-2026-5-11-19-39-0.md | 10 +++ .../src/Providers/ClientProvider.cs | 11 +++- .../ClientProviders/ClientProviderTests.cs | 61 +++++++++++++++++++ ...ltipleNewOptionalNonBodyParametersAdded.cs | 4 +- ...bility_NewOptionalNonBodyParameterAdded.cs | 4 +- ...ionalNonBodyParameterAddedWithModelBody.cs | 4 +- ...rameterAddedWithPathAndHeaderParameters.cs | 4 +- .../TestClient.cs | 21 +++++++ .../src/Shared/MethodSignatureHelper.cs | 20 +++++- .../generator/docs/backward-compatibility.md | 4 ++ 10 files changed, 130 insertions(+), 13 deletions(-) create mode 100644 .chronus/changes/fix-csharp-backcompat-reserved-names-and-cancellation-token-2026-5-11-19-39-0.md create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedNameAndCancellationToken/TestClient.cs diff --git a/.chronus/changes/fix-csharp-backcompat-reserved-names-and-cancellation-token-2026-5-11-19-39-0.md b/.chronus/changes/fix-csharp-backcompat-reserved-names-and-cancellation-token-2026-5-11-19-39-0.md new file mode 100644 index 00000000000..5a7f7ba81dc --- /dev/null +++ b/.chronus/changes/fix-csharp-backcompat-reserved-names-and-cancellation-token-2026-5-11-19-39-0.md @@ -0,0 +1,10 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-csharp" +--- + +Fix back-compat overloads generated for service methods so that: + +- Named-argument labels in the delegating call use the C# variable name when the previous parameter's raw input name is not a valid C# identifier (for example OData query parameters such as `$select`, `$top`, `$skip`, `$count`). +- A trailing `CancellationToken cancellationToken` parameter is preserved as optional (`= default`) on the back-compat overload so the generated method continues to satisfy the SDK guideline that requires client convenience methods to end with an optional `CancellationToken`. 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 85f6a268220..68e68678d1e 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 @@ -1882,19 +1882,24 @@ private static bool HasNewOptionalNonBodyParametersOnly( var previousSignature = previousMethod.Signature; var currentSignature = currentMethod.Signature; + // Match parameters by their C# variable name (which is what is rendered in C# source) so + // that previous parameters whose raw input name differs only by reserved characters such as + // a leading '$' (e.g. OData "$select"/"$top") are matched to the corresponding current + // parameter. The named-argument label below must also use the C# variable name. var previousParamsByName = new Dictionary(); foreach (var p in previousSignature.Parameters) { - previousParamsByName.TryAdd(p.Name, p); + previousParamsByName.TryAdd(p.Name.ToVariableName(), p); } var arguments = new List(currentSignature.Parameters.Count); foreach (var currentParam in currentSignature.Parameters) { - ValueExpression value = previousParamsByName.TryGetValue(currentParam.Name, out var prevParam) + var currentParamVariableName = currentParam.Name.ToVariableName(); + ValueExpression value = previousParamsByName.TryGetValue(currentParamVariableName, out var prevParam) ? prevParam : (currentParam.DefaultValue ?? Default); - arguments.Add(PositionalReference(currentParam.Name, value)); + arguments.Add(PositionalReference(currentParamVariableName, value)); } return new ScmMethodProvider( 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 48275b97be9..244140e4fc3 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 @@ -3407,6 +3407,67 @@ public async Task BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndH Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + // Last contract has GetData(int param1, string param2, CancellationToken) (and async). + // The current TypeSpec adds new optional non-body query parameters whose raw input names start + // with reserved characters (e.g. OData "$select", "$top"). The generated back-compat overload + // must: + // 1. Use the C# variable name (e.g. "select", "top") for the named-argument labels in the + // delegating call; it must NOT use the raw "$select"/"$top" names which would produce + // invalid C#. + // 2. Keep the trailing `CancellationToken cancellationToken` parameter optional so the + // generated method does not violate the SDK guideline that requires client methods to end + // with an optional CancellationToken (or a RequestContext context). + [Test] + public async Task BackCompatibility_NewOptionalParameterWithReservedNameAndCancellationToken() + { + var param1 = InputFactory.QueryParameter("param1", InputPrimitiveType.Int32, isRequired: true); + var param2 = InputFactory.BodyParameter("param2", InputPrimitiveType.String, isRequired: true); + var selectParam = InputFactory.QueryParameter("$select", InputPrimitiveType.String, isRequired: false); + var topParam = InputFactory.QueryParameter("$top", InputPrimitiveType.Int32, isRequired: false); + + var operation = InputFactory.Operation( + "GetData", + parameters: [param1, param2, selectParam, topParam], + 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("$select", InputPrimitiveType.String, location: InputRequestLocation.Query, isRequired: false), + InputFactory.MethodParameter("$top", InputPrimitiveType.Int32, 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(); + var content = file.Content; + + // The named-argument labels in the back-compat body must use the C# variable names, NOT the + // raw "$select"/"$top" names, which would produce invalid C#. + StringAssert.Contains("select: default", content); + StringAssert.Contains("top: default", content); + StringAssert.DoesNotContain("$select:", content); + StringAssert.DoesNotContain("$top:", content); + + // The back-compat overloads must keep the trailing CancellationToken optional. + StringAssert.Contains("CancellationToken cancellationToken = default)", content); + StringAssert.DoesNotContain("CancellationToken cancellationToken)", 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 index f1caf7a33e4..2b29da81951 100644 --- 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 @@ -48,13 +48,13 @@ public partial class TestClient } [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) + public virtual global::System.ClientModel.ClientResult GetData(int param1, string param2, global::System.Threading.CancellationToken cancellationToken = default) { 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) + public virtual global::System.Threading.Tasks.Task> GetDataAsync(int param1, string param2, global::System.Threading.CancellationToken cancellationToken = default) { 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_NewOptionalNonBodyParameterAdded.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAdded.cs index 3050a4a5b27..1e2a103a8df 100644 --- 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 @@ -48,13 +48,13 @@ public partial class TestClient } [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) + public virtual global::System.ClientModel.ClientResult GetData(int param1, string param2, global::System.Threading.CancellationToken cancellationToken = default) { 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) + public virtual global::System.Threading.Tasks.Task> GetDataAsync(int param1, string param2, global::System.Threading.CancellationToken cancellationToken = default) { 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_NewOptionalNonBodyParameterAddedWithModelBody.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody.cs index 7abd57c6eeb..7e73c7cae7d 100644 --- 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 @@ -46,13 +46,13 @@ public partial class TestClient } [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) + public virtual global::System.ClientModel.ClientResult GetData(int param1, global::Sample.Models.SampleModel body, global::System.Threading.CancellationToken cancellationToken = default) { 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) + public virtual global::System.Threading.Tasks.Task> GetDataAsync(int param1, global::Sample.Models.SampleModel body, global::System.Threading.CancellationToken cancellationToken = default) { 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_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters.cs index ecd5a27c74a..3956b02e8bc 100644 --- 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 @@ -49,13 +49,13 @@ public partial class TestClient } [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) + public virtual global::System.ClientModel.ClientResult GetData(string itemId, int filter, string region, global::System.Threading.CancellationToken cancellationToken = default) { 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) + public virtual global::System.Threading.Tasks.Task> GetDataAsync(string itemId, int filter, string region, global::System.Threading.CancellationToken cancellationToken = default) { 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_NewOptionalParameterWithReservedNameAndCancellationToken/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedNameAndCancellationToken/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_NewOptionalParameterWithReservedNameAndCancellationToken/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/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index 787804555af..aca36b687a5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Threading; using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; @@ -57,9 +58,17 @@ internal static MethodSignature BuildBackCompatMethodSignature(MethodSignature p { if (hideMethod) { - // make all parameter required to avoid ambiguous call sites if necessary - foreach (var param in previousMethodSignature.Parameters) + // make all parameter required to avoid ambiguous call sites if necessary, except for a + // trailing CancellationToken named "cancellationToken" which must remain optional to + // satisfy the SDK guideline that requires client methods to end with an optional + // CancellationToken parameter (or a RequestContext context). + for (int i = 0; i < previousMethodSignature.Parameters.Count; i++) { + var param = previousMethodSignature.Parameters[i]; + if (i == previousMethodSignature.Parameters.Count - 1 && IsOptionalCancellationTokenParameter(param)) + { + continue; + } param.DefaultValue = null; } } @@ -77,6 +86,13 @@ internal static MethodSignature BuildBackCompatMethodSignature(MethodSignature p Attributes: attributes); } + internal static bool IsOptionalCancellationTokenParameter(ParameterProvider parameter) + { + return parameter.DefaultValue is not null + && parameter.Name.ToVariableName() == "cancellationToken" + && parameter.Type.AreNamesEqual(typeof(CancellationToken)); + } + private sealed class ParameterProviderVariableNameComparer : IEqualityComparer { public bool Equals(ParameterProvider? x, ParameterProvider? y) diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index f4b0c837165..c3da2299a9b 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -868,3 +868,7 @@ public virtual Task GetDataAsync(int p1, BinaryContent body, Reque ``` 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. + +A trailing `CancellationToken cancellationToken` parameter is preserved as optional (`= default`) on the back-compat overload so that the generated method continues to satisfy the [Azure SDK guideline](https://azure.github.io/azure-sdk/dotnet_implementation.html#parameter-presence-and-ordering) that requires client convenience methods to end with an optional `CancellationToken` parameter. + +When the previous method's parameter has a raw input name that contains characters that are not valid in a C# identifier (for example OData query parameters such as `$select`, `$top`, `$skip`, `$count`), the named-argument labels in the delegating call use the C# variable name (e.g. `select: default`, `top: default`) rather than the raw input name. From 9a5e6e9e4737afe0b404f448b04afad59eb75d0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 19:50:29 +0000 Subject: [PATCH 3/9] Strengthen back-compat reserved-name test to verify default-stripping on non-trailing params Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/09ec7e5f-2d2f-4a58-8662-7d148879bb09 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../test/Providers/ClientProviders/ClientProviderTests.cs | 5 +++++ 1 file changed, 5 insertions(+) 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 244140e4fc3..378611de4f7 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 @@ -3466,6 +3466,11 @@ public async Task BackCompatibility_NewOptionalParameterWithReservedNameAndCance // The back-compat overloads must keep the trailing CancellationToken optional. StringAssert.Contains("CancellationToken cancellationToken = default)", content); StringAssert.DoesNotContain("CancellationToken cancellationToken)", content); + + // The non-trailing parameters of the back-compat overload must have their defaults stripped + // so they are required (param1 is `int` and param2 is `string`, neither should carry `= default`). + StringAssert.Contains("GetData(int param1, string param2, global::System.Threading.CancellationToken cancellationToken = default)", content); + StringAssert.Contains("GetDataAsync(int param1, string param2, global::System.Threading.CancellationToken cancellationToken = default)", content); } [Test] From 4bc50b5838fe44ba7c11d3af18efd9c32ff06f73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 20:12:50 +0000 Subject: [PATCH 4/9] Revert CancellationToken default-preservation; keep reserved-name fix; use testdata for new test Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/3f9baf01-88a8-4b30-a7ca-d0a5b5897fe4 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- ...nd-cancellation-token-2026-5-11-19-39-0.md | 10 --- .../ClientProviders/ClientProviderTests.cs | 30 ++------- ...ltipleNewOptionalNonBodyParametersAdded.cs | 4 +- ...bility_NewOptionalNonBodyParameterAdded.cs | 4 +- ...ionalNonBodyParameterAddedWithModelBody.cs | 4 +- ...rameterAddedWithPathAndHeaderParameters.cs | 4 +- ...ty_NewOptionalParameterWithReservedName.cs | 62 +++++++++++++++++++ .../TestClient.cs | 0 .../src/Shared/MethodSignatureHelper.cs | 20 +----- .../generator/docs/backward-compatibility.md | 4 -- 10 files changed, 77 insertions(+), 65 deletions(-) delete mode 100644 .chronus/changes/fix-csharp-backcompat-reserved-names-and-cancellation-token-2026-5-11-19-39-0.md create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName.cs rename packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/{BackCompatibility_NewOptionalParameterWithReservedNameAndCancellationToken => BackCompatibility_NewOptionalParameterWithReservedName}/TestClient.cs (100%) diff --git a/.chronus/changes/fix-csharp-backcompat-reserved-names-and-cancellation-token-2026-5-11-19-39-0.md b/.chronus/changes/fix-csharp-backcompat-reserved-names-and-cancellation-token-2026-5-11-19-39-0.md deleted file mode 100644 index 5a7f7ba81dc..00000000000 --- a/.chronus/changes/fix-csharp-backcompat-reserved-names-and-cancellation-token-2026-5-11-19-39-0.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/http-client-csharp" ---- - -Fix back-compat overloads generated for service methods so that: - -- Named-argument labels in the delegating call use the C# variable name when the previous parameter's raw input name is not a valid C# identifier (for example OData query parameters such as `$select`, `$top`, `$skip`, `$count`). -- A trailing `CancellationToken cancellationToken` parameter is preserved as optional (`= default`) on the back-compat overload so the generated method continues to satisfy the SDK guideline that requires client convenience methods to end with an optional `CancellationToken`. 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 378611de4f7..57b8ef6a3ba 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 @@ -3410,15 +3410,11 @@ public async Task BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndH // Last contract has GetData(int param1, string param2, CancellationToken) (and async). // The current TypeSpec adds new optional non-body query parameters whose raw input names start // with reserved characters (e.g. OData "$select", "$top"). The generated back-compat overload - // must: - // 1. Use the C# variable name (e.g. "select", "top") for the named-argument labels in the - // delegating call; it must NOT use the raw "$select"/"$top" names which would produce - // invalid C#. - // 2. Keep the trailing `CancellationToken cancellationToken` parameter optional so the - // generated method does not violate the SDK guideline that requires client methods to end - // with an optional CancellationToken (or a RequestContext context). + // must use the C# variable name (e.g. "select", "top") for the named-argument labels in the + // delegating call; it must NOT use the raw "$select"/"$top" names which would produce invalid + // C#. [Test] - public async Task BackCompatibility_NewOptionalParameterWithReservedNameAndCancellationToken() + public async Task BackCompatibility_NewOptionalParameterWithReservedName() { var param1 = InputFactory.QueryParameter("param1", InputPrimitiveType.Int32, isRequired: true); var param2 = InputFactory.BodyParameter("param2", InputPrimitiveType.String, isRequired: true); @@ -3454,23 +3450,7 @@ public async Task BackCompatibility_NewOptionalParameterWithReservedNameAndCance var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider(clientProvider!, name => name == "GetData" || name == "GetDataAsync")); var file = writer.Write(); - var content = file.Content; - - // The named-argument labels in the back-compat body must use the C# variable names, NOT the - // raw "$select"/"$top" names, which would produce invalid C#. - StringAssert.Contains("select: default", content); - StringAssert.Contains("top: default", content); - StringAssert.DoesNotContain("$select:", content); - StringAssert.DoesNotContain("$top:", content); - - // The back-compat overloads must keep the trailing CancellationToken optional. - StringAssert.Contains("CancellationToken cancellationToken = default)", content); - StringAssert.DoesNotContain("CancellationToken cancellationToken)", content); - - // The non-trailing parameters of the back-compat overload must have their defaults stripped - // so they are required (param1 is `int` and param2 is `string`, neither should carry `= default`). - StringAssert.Contains("GetData(int param1, string param2, global::System.Threading.CancellationToken cancellationToken = default)", content); - StringAssert.Contains("GetDataAsync(int param1, string param2, global::System.Threading.CancellationToken cancellationToken = default)", content); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } [Test] 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 index 2b29da81951..f1caf7a33e4 100644 --- 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 @@ -48,13 +48,13 @@ public partial class TestClient } [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 = default) + 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 = default) + 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_NewOptionalNonBodyParameterAdded.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAdded.cs index 1e2a103a8df..3050a4a5b27 100644 --- 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 @@ -48,13 +48,13 @@ public partial class TestClient } [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 = default) + 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 = default) + 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_NewOptionalNonBodyParameterAddedWithModelBody.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody.cs index 7e73c7cae7d..7abd57c6eeb 100644 --- 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 @@ -46,13 +46,13 @@ public partial class TestClient } [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 = default) + 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 = default) + 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_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters.cs index 3956b02e8bc..ecd5a27c74a 100644 --- 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 @@ -49,13 +49,13 @@ public partial class TestClient } [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 = default) + 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 = default) + 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_NewOptionalParameterWithReservedName.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName.cs new file mode 100644 index 00000000000..1516731b261 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName.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, string @select = default, int? top = 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, @select, top, 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, string @select = default, int? top = 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, @select, top, 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, string @select = default, int? top = 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, @select, top, 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, string @select = default, int? top = 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, @select, top, 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, @select: default, top: 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, @select: default, top: default, cancellationToken: cancellationToken); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedNameAndCancellationToken/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs similarity index 100% rename from packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedNameAndCancellationToken/TestClient.cs rename to packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index aca36b687a5..787804555af 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Threading; using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; @@ -58,17 +57,9 @@ internal static MethodSignature BuildBackCompatMethodSignature(MethodSignature p { if (hideMethod) { - // make all parameter required to avoid ambiguous call sites if necessary, except for a - // trailing CancellationToken named "cancellationToken" which must remain optional to - // satisfy the SDK guideline that requires client methods to end with an optional - // CancellationToken parameter (or a RequestContext context). - for (int i = 0; i < previousMethodSignature.Parameters.Count; i++) + // make all parameter required to avoid ambiguous call sites if necessary + foreach (var param in previousMethodSignature.Parameters) { - var param = previousMethodSignature.Parameters[i]; - if (i == previousMethodSignature.Parameters.Count - 1 && IsOptionalCancellationTokenParameter(param)) - { - continue; - } param.DefaultValue = null; } } @@ -86,13 +77,6 @@ internal static MethodSignature BuildBackCompatMethodSignature(MethodSignature p Attributes: attributes); } - internal static bool IsOptionalCancellationTokenParameter(ParameterProvider parameter) - { - return parameter.DefaultValue is not null - && parameter.Name.ToVariableName() == "cancellationToken" - && parameter.Type.AreNamesEqual(typeof(CancellationToken)); - } - private sealed class ParameterProviderVariableNameComparer : IEqualityComparer { public bool Equals(ParameterProvider? x, ParameterProvider? y) diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index c3da2299a9b..f4b0c837165 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -868,7 +868,3 @@ public virtual Task GetDataAsync(int p1, BinaryContent body, Reque ``` 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. - -A trailing `CancellationToken cancellationToken` parameter is preserved as optional (`= default`) on the back-compat overload so that the generated method continues to satisfy the [Azure SDK guideline](https://azure.github.io/azure-sdk/dotnet_implementation.html#parameter-presence-and-ordering) that requires client convenience methods to end with an optional `CancellationToken` parameter. - -When the previous method's parameter has a raw input name that contains characters that are not valid in a C# identifier (for example OData query parameters such as `$select`, `$top`, `$skip`, `$count`), the named-argument labels in the delegating call use the C# variable name (e.g. `select: default`, `top: default`) rather than the raw input name. From 671cae711f16c1082f5f6c524f257c55a383772a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 21:38:49 +0000 Subject: [PATCH 5/9] Skip back-compat overload when previous method ends in CancellationToken Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/6dbf78c7-b80c-4935-9257-76966b6f1923 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 28 +++++++++ .../ClientProviders/ClientProviderTests.cs | 46 -------------- ...ltipleNewOptionalNonBodyParametersAdded.cs | 13 ---- ...bility_NewOptionalNonBodyParameterAdded.cs | 13 ---- ...ionalNonBodyParameterAddedWithModelBody.cs | 13 ---- ...rameterAddedWithPathAndHeaderParameters.cs | 13 ---- ...ty_NewOptionalParameterWithReservedName.cs | 62 ------------------- .../TestClient.cs | 21 ------- 8 files changed, 28 insertions(+), 181 deletions(-) delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs 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 68e68678d1e..7a18d50c58d 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 @@ -1812,6 +1812,23 @@ private void ProcessBackCompatForNewOptionalParameters( continue; } + // Skip generating the back-compat overload when the previous method's trailing parameter is a + // CancellationToken. There is no way to emit a correct overload in that situation: + // - Keeping the trailing CancellationToken optional (preserving the SDK guideline) introduces + // a CS0121 ambiguity at basic call sites between the back-compat overload and the new + // method (which also ends in an optional CancellationToken with extra optional params + // before it). + // - Making the trailing CancellationToken required avoids the ambiguity but violates the + // SDK guideline that CancellationToken parameters must be optional. + // In this case the library author must address the gap via custom code or by updating the spec. + if (PreviousMethodEndsWithCancellationToken(previousSignature)) + { + CodeModelGenerator.Instance.Emitter.Debug( + $"Skipped back-compat overload for '{Name}.{previousSignature.Name}' because the previous method's trailing CancellationToken parameter would result in either an ambiguous reference or a violation of the CancellationToken-must-be-optional SDK guideline.", + BackCompatibilityChangeCategory.SvcMethodNewOptionalParameterOverloadAdded); + continue; + } + var overload = BuildBackCompatOverloadForNewOptionalParameters(previousMethod, matchedCurrent); if (overload == null || !currentMethodSignatures.TryAdd(overload.Signature, overload)) { @@ -1825,6 +1842,17 @@ private void ProcessBackCompatForNewOptionalParameters( } } + private static bool PreviousMethodEndsWithCancellationToken(MethodSignature previousSignature) + { + if (previousSignature.Parameters.Count == 0) + { + return false; + } + + var lastParam = previousSignature.Parameters[previousSignature.Parameters.Count - 1]; + return lastParam.Type.Equals(typeof(CancellationToken)); + } + // 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( 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 57b8ef6a3ba..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 @@ -3407,52 +3407,6 @@ public async Task BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndH Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } - // Last contract has GetData(int param1, string param2, CancellationToken) (and async). - // The current TypeSpec adds new optional non-body query parameters whose raw input names start - // with reserved characters (e.g. OData "$select", "$top"). The generated back-compat overload - // must use the C# variable name (e.g. "select", "top") for the named-argument labels in the - // delegating call; it must NOT use the raw "$select"/"$top" names which would produce invalid - // C#. - [Test] - public async Task BackCompatibility_NewOptionalParameterWithReservedName() - { - var param1 = InputFactory.QueryParameter("param1", InputPrimitiveType.Int32, isRequired: true); - var param2 = InputFactory.BodyParameter("param2", InputPrimitiveType.String, isRequired: true); - var selectParam = InputFactory.QueryParameter("$select", InputPrimitiveType.String, isRequired: false); - var topParam = InputFactory.QueryParameter("$top", InputPrimitiveType.Int32, isRequired: false); - - var operation = InputFactory.Operation( - "GetData", - parameters: [param1, param2, selectParam, topParam], - 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("$select", InputPrimitiveType.String, location: InputRequestLocation.Query, isRequired: false), - InputFactory.MethodParameter("$top", InputPrimitiveType.Int32, 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 index f1caf7a33e4..888e61d9a6b 100644 --- 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 @@ -5,7 +5,6 @@ using System; using System.ClientModel; using System.ClientModel.Primitives; -using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -46,17 +45,5 @@ public partial class TestClient 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_NewOptionalNonBodyParameterAdded.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAdded.cs index 3050a4a5b27..a6e441f4c81 100644 --- 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 @@ -5,7 +5,6 @@ using System; using System.ClientModel; using System.ClientModel.Primitives; -using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -46,17 +45,5 @@ public partial class TestClient 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_NewOptionalNonBodyParameterAddedWithModelBody.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithModelBody.cs index 7abd57c6eeb..eddd47f6c95 100644 --- 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 @@ -4,7 +4,6 @@ using System.ClientModel; using System.ClientModel.Primitives; -using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Sample.Models; @@ -44,17 +43,5 @@ public partial class TestClient 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_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndHeaderParameters.cs index ecd5a27c74a..d99efc9dd3a 100644 --- 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 @@ -4,7 +4,6 @@ using System.ClientModel; using System.ClientModel.Primitives; -using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -47,17 +46,5 @@ public partial class TestClient 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_NewOptionalParameterWithReservedName.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName.cs deleted file mode 100644 index 1516731b261..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName.cs +++ /dev/null @@ -1,62 +0,0 @@ -// - -#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, string @select = default, int? top = 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, @select, top, 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, string @select = default, int? top = 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, @select, top, 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, string @select = default, int? top = 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, @select, top, 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, string @select = default, int? top = 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, @select, top, 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, @select: default, top: 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, @select: default, top: default, cancellationToken: cancellationToken); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs deleted file mode 100644 index 5bca61e2cea..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs +++ /dev/null @@ -1,21 +0,0 @@ -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(); - } - } -} From 712dd5a6e9687d1f6580c9c26d174a2c08416555 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 23:50:07 +0000 Subject: [PATCH 6/9] Rename helper to PreviousSignatureEndsWithCancellationToken Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/e8de2bea-833b-4054-a871-6cf5fd2e23f4 Co-authored-by: JonathanCrd <17486462+JonathanCrd@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7a18d50c58d..4d24793751b 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 @@ -1821,7 +1821,7 @@ private void ProcessBackCompatForNewOptionalParameters( // - Making the trailing CancellationToken required avoids the ambiguity but violates the // SDK guideline that CancellationToken parameters must be optional. // In this case the library author must address the gap via custom code or by updating the spec. - if (PreviousMethodEndsWithCancellationToken(previousSignature)) + if (PreviousSignatureEndsWithCancellationToken(previousSignature)) { CodeModelGenerator.Instance.Emitter.Debug( $"Skipped back-compat overload for '{Name}.{previousSignature.Name}' because the previous method's trailing CancellationToken parameter would result in either an ambiguous reference or a violation of the CancellationToken-must-be-optional SDK guideline.", @@ -1842,7 +1842,7 @@ private void ProcessBackCompatForNewOptionalParameters( } } - private static bool PreviousMethodEndsWithCancellationToken(MethodSignature previousSignature) + private static bool PreviousSignatureEndsWithCancellationToken(MethodSignature previousSignature) { if (previousSignature.Parameters.Count == 0) { From 2cfd63958036bc58312216beb967286e9756897a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 23:05:28 +0000 Subject: [PATCH 7/9] Address review: remove long comment, use IgnoreNullableComparer, add reserved-name test Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/439261fa-3acd-4492-94bc-8c1fbecb77f7 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 18 ++---- .../ClientProviders/ClientProviderTests.cs | 44 +++++++++++++ ...ty_NewOptionalParameterWithReservedName.cs | 62 +++++++++++++++++++ .../TestClient.cs | 20 ++++++ 4 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs 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 4d24793751b..7943d8fa8cc 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 @@ -1812,15 +1812,6 @@ private void ProcessBackCompatForNewOptionalParameters( continue; } - // Skip generating the back-compat overload when the previous method's trailing parameter is a - // CancellationToken. There is no way to emit a correct overload in that situation: - // - Keeping the trailing CancellationToken optional (preserving the SDK guideline) introduces - // a CS0121 ambiguity at basic call sites between the back-compat overload and the new - // method (which also ends in an optional CancellationToken with extra optional params - // before it). - // - Making the trailing CancellationToken required avoids the ambiguity but violates the - // SDK guideline that CancellationToken parameters must be optional. - // In this case the library author must address the gap via custom code or by updating the spec. if (PreviousSignatureEndsWithCancellationToken(previousSignature)) { CodeModelGenerator.Instance.Emitter.Debug( @@ -1842,6 +1833,9 @@ private void ProcessBackCompatForNewOptionalParameters( } } + private static readonly IEqualityComparer s_ignoreNullableTypeComparer = new CSharpType.CSharpTypeIgnoreNullableComparer(); + private static readonly CSharpType s_cancellationTokenType = new(typeof(CancellationToken)); + private static bool PreviousSignatureEndsWithCancellationToken(MethodSignature previousSignature) { if (previousSignature.Parameters.Count == 0) @@ -1850,7 +1844,7 @@ private static bool PreviousSignatureEndsWithCancellationToken(MethodSignature p } var lastParam = previousSignature.Parameters[previousSignature.Parameters.Count - 1]; - return lastParam.Type.Equals(typeof(CancellationToken)); + return s_ignoreNullableTypeComparer.Equals(lastParam.Type, s_cancellationTokenType); } // Returns true when currentSignature contains all parameters of previousSignature in the same @@ -1910,10 +1904,6 @@ private static bool HasNewOptionalNonBodyParametersOnly( var previousSignature = previousMethod.Signature; var currentSignature = currentMethod.Signature; - // Match parameters by their C# variable name (which is what is rendered in C# source) so - // that previous parameters whose raw input name differs only by reserved characters such as - // a leading '$' (e.g. OData "$select"/"$top") are matched to the corresponding current - // parameter. The named-argument label below must also use the C# variable name. var previousParamsByName = new Dictionary(); foreach (var p in previousSignature.Parameters) { 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 48275b97be9..5f2fe299185 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 @@ -3407,6 +3407,50 @@ public async Task BackCompatibility_NewOptionalNonBodyParameterAddedWithPathAndH Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + // Last contract has only protocol methods: GetData(int param1, BinaryContent content, RequestOptions options = null). + // The current TypeSpec adds a new optional non-body query parameter "$select" whose raw input name + // starts with a reserved character. + // Expected: a hidden back-compat protocol overload matching the previous signature is added. The + // delegating call body must use the C# variable name ("select") for the named-argument label, not + // the raw "$select" name (which would produce invalid C#). + [Test] + public async Task BackCompatibility_NewOptionalParameterWithReservedName() + { + var param1 = InputFactory.QueryParameter("param1", InputPrimitiveType.Int32, isRequired: true); + var content = InputFactory.BodyParameter("content", InputPrimitiveType.String, isRequired: true); + var selectParam = InputFactory.QueryParameter("$select", InputPrimitiveType.String, isRequired: false); + + var operation = InputFactory.Operation( + "GetData", + parameters: [param1, content, selectParam], + responses: [InputFactory.OperationResponse([200], bodytype: InputPrimitiveType.String)]); + + List methodParameters = + [ + InputFactory.MethodParameter("param1", InputPrimitiveType.Int32, location: InputRequestLocation.Query, isRequired: true), + InputFactory.MethodParameter("content", InputPrimitiveType.String, location: InputRequestLocation.Body, isRequired: true), + InputFactory.MethodParameter("$select", 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_NewOptionalParameterWithReservedName.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName.cs new file mode 100644 index 00000000000..ffa25774611 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName.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, string @select = 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, @select, 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, string @select = 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, @select, options); + return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + + public virtual global::System.ClientModel.ClientResult GetData(int param1, string content, string @select = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(content, nameof(content)); + + using global::System.ClientModel.BinaryContent content0 = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(content)); + global::System.ClientModel.ClientResult result = this.GetData(param1, content0, @select, 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 content, string @select = default, global::System.Threading.CancellationToken cancellationToken = default) + { + global::Sample.Argument.AssertNotNullOrEmpty(content, nameof(content)); + + using global::System.ClientModel.BinaryContent content0 = global::System.ClientModel.BinaryContent.Create(global::System.BinaryData.FromString(content)); + global::System.ClientModel.ClientResult result = await this.GetDataAsync(param1, content0, @select, 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::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options) + { + return this.GetData(param1: param1, content: content, @select: default, options: options); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public virtual global::System.Threading.Tasks.Task GetDataAsync(int param1, global::System.ClientModel.BinaryContent content, global::System.ClientModel.Primitives.RequestOptions options) + { + return this.GetDataAsync(param1: param1, content: content, @select: default, options: options); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs new file mode 100644 index 00000000000..a9f9673e177 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/BackCompatibility_NewOptionalParameterWithReservedName/TestClient.cs @@ -0,0 +1,20 @@ +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading.Tasks; + +namespace Sample +{ + public partial class TestClient + { + public virtual ClientResult GetData(int param1, BinaryContent content, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public virtual Task GetDataAsync(int param1, BinaryContent content, RequestOptions options = null) + { + throw new NotImplementedException(); + } + } +} From 1b86d2d91ac69cd41c8114fe6e1f357077f65ab9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 23:12:34 +0000 Subject: [PATCH 8/9] Inline CSharpType comparer/CancellationToken type instead of static fields Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/8776c9f9-3f7d-4702-8d18-00b68c25ec16 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 7943d8fa8cc..a43254b9ea9 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 @@ -1833,9 +1833,6 @@ private void ProcessBackCompatForNewOptionalParameters( } } - private static readonly IEqualityComparer s_ignoreNullableTypeComparer = new CSharpType.CSharpTypeIgnoreNullableComparer(); - private static readonly CSharpType s_cancellationTokenType = new(typeof(CancellationToken)); - private static bool PreviousSignatureEndsWithCancellationToken(MethodSignature previousSignature) { if (previousSignature.Parameters.Count == 0) @@ -1844,7 +1841,7 @@ private static bool PreviousSignatureEndsWithCancellationToken(MethodSignature p } var lastParam = previousSignature.Parameters[previousSignature.Parameters.Count - 1]; - return s_ignoreNullableTypeComparer.Equals(lastParam.Type, s_cancellationTokenType); + return new CSharpType.CSharpTypeIgnoreNullableComparer().Equals(lastParam.Type, new CSharpType(typeof(CancellationToken))); } // Returns true when currentSignature contains all parameters of previousSignature in the same From e89375b5f52f51dc508e20eea68f6082389d2caa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 23:56:10 +0000 Subject: [PATCH 9/9] Wrap back-compat overload in #pragma AZC0002 disable instead of skipping when previous method ends with CancellationToken Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/cfe7f3df-2dc7-4049-b5a3-48583727e070 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 17 ++++++++++------- ...MultipleNewOptionalNonBodyParametersAdded.cs | 17 +++++++++++++++++ ...tibility_NewOptionalNonBodyParameterAdded.cs | 17 +++++++++++++++++ ...ptionalNonBodyParameterAddedWithModelBody.cs | 17 +++++++++++++++++ ...ParameterAddedWithPathAndHeaderParameters.cs | 17 +++++++++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) 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 a43254b9ea9..882b167457f 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 @@ -1812,18 +1812,21 @@ private void ProcessBackCompatForNewOptionalParameters( continue; } - if (PreviousSignatureEndsWithCancellationToken(previousSignature)) + var overload = BuildBackCompatOverloadForNewOptionalParameters(previousMethod, matchedCurrent); + if (overload == null || !currentMethodSignatures.TryAdd(overload.Signature, overload)) { - CodeModelGenerator.Instance.Emitter.Debug( - $"Skipped back-compat overload for '{Name}.{previousSignature.Name}' because the previous method's trailing CancellationToken parameter would result in either an ambiguous reference or a violation of the CancellationToken-must-be-optional SDK guideline.", - BackCompatibilityChangeCategory.SvcMethodNewOptionalParameterOverloadAdded); continue; } - var overload = BuildBackCompatOverloadForNewOptionalParameters(previousMethod, matchedCurrent); - if (overload == null || !currentMethodSignatures.TryAdd(overload.Signature, overload)) + if (PreviousSignatureEndsWithCancellationToken(previousSignature)) { - continue; + overload.Update(suppressions: + [ + new SuppressionStatement( + inner: null, + code: Literal("AZC0002"), + justification: "Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method.") + ]); } methods.Add(overload); 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 index 888e61d9a6b..ecd8a142b0c 100644 --- 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 @@ -5,6 +5,7 @@ using System; using System.ClientModel; using System.ClientModel.Primitives; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -45,5 +46,21 @@ public partial class TestClient 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()); } + +#pragma warning disable AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + [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); + } +#pragma warning restore AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + +#pragma warning disable AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + [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); + } +#pragma warning restore AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. } } 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 index a6e441f4c81..dec0cb1513e 100644 --- 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 @@ -5,6 +5,7 @@ using System; using System.ClientModel; using System.ClientModel.Primitives; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -45,5 +46,21 @@ public partial class TestClient 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()); } + +#pragma warning disable AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + [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); + } +#pragma warning restore AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + +#pragma warning disable AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + [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); + } +#pragma warning restore AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. } } 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 index eddd47f6c95..c1f11b6273c 100644 --- 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 @@ -4,6 +4,7 @@ using System.ClientModel; using System.ClientModel.Primitives; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Sample.Models; @@ -43,5 +44,21 @@ public partial class TestClient 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()); } + +#pragma warning disable AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + [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); + } +#pragma warning restore AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + +#pragma warning disable AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + [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); + } +#pragma warning restore AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. } } 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 index d99efc9dd3a..f97dddf4f4a 100644 --- 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 @@ -4,6 +4,7 @@ using System.ClientModel; using System.ClientModel.Primitives; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -46,5 +47,21 @@ public partial class TestClient 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()); } + +#pragma warning disable AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + [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); + } +#pragma warning restore AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + +#pragma warning disable AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. + [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); + } +#pragma warning restore AZC0002 // Back-compat overload preserves the previous method signature where CancellationToken was the trailing parameter. Making it optional would introduce an ambiguous call with the new method. } }