From b35d7af7337ea5a38af9027218c377b1501588d2 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 13 Feb 2026 18:29:49 -0800 Subject: [PATCH 1/5] Add backcompat target for client --- .../src/Providers/ClientProvider.cs | 26 ++++- .../src/Providers/RestClientProvider.cs | 23 +++-- .../ListPageableTests.cs | 94 +++++++++++++++++++ .../MockableTestResource.cs | 18 ++++ 4 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/ListPageableTests/TopParameterPreservedViaBackCompatProvider/MockableTestResource.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 a87e1b5936a..01287f39d44 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 @@ -58,6 +58,13 @@ private record ApiVersionFields(FieldProvider Field, PropertyProvider? Correspon private Dictionary? _methodCache; private Dictionary MethodCache => _methodCache ??= []; + private TypeProvider? _backCompatProvider; + + /// + /// Gets the effective type provider to use for backward compatibility checks. + /// When a is set, it is used instead of this client. + /// + internal TypeProvider BackCompatProvider => _backCompatProvider ?? this; public ParameterProvider? ClientOptionsParameter { get; } @@ -835,8 +842,25 @@ [.. primaryCtorOrderedParams.Select(p => secondaryParamNames.Contains(p.Name) ? this); } - public ScmMethodProviderCollection GetMethodCollectionByOperation(InputOperation operation) + public ScmMethodProviderCollection GetMethodCollectionByOperation(InputOperation operation, TypeProvider? backCompatProvider = null) { + if (backCompatProvider != null && backCompatProvider != this) + { + if (_backCompatProvider != backCompatProvider) + { + _backCompatProvider = backCompatProvider; + // reset cache so methods are rebuilt with the new backcompat provider + Reset(); + _methodCache = null; + } + } + else if (_backCompatProvider != null) + { + // backcompat provider was previously set but not requested now — reset to default + _backCompatProvider = null; + Reset(); + _methodCache = null; + } _ = Methods; // Ensure methods are built return MethodCache[operation]; } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index b7d15ea32e8..2e9a5b85510 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -875,12 +875,14 @@ private static bool TryGetSpecialHeaderParam(InputParameter inputParameter, [Not : null; } - private static void UpdateParameterNameWithBackCompat(InputParameter inputParameter, string proposedName, ClientProvider client) + private static void UpdateParameterNameWithBackCompat(InputParameter inputParameter, string proposedName, TypeProvider backCompatProvider) { - // Check if the original parameter name exists in LastContractView for backward compatibility - var existingParam = client.LastContractView?.Methods + // Check if the original wire name exists in LastContractView for backward compatibility. + // We use SerializedName (the wire name) rather than Name because Name may have been mutated + // by a previous call to this method. + var existingParam = backCompatProvider.LastContractView?.Methods ?.SelectMany(method => method.Signature.Parameters) - .FirstOrDefault(p => string.Equals(p.Name, inputParameter.Name, StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(p => string.Equals(p.Name, inputParameter.SerializedName, StringComparison.OrdinalIgnoreCase)) ?.Name; if (existingParam != null) @@ -1012,21 +1014,24 @@ internal static List GetMethodParameters( // For paging operations, handle parameter name corrections with backward compatibility if (serviceMethod is InputPagingServiceMethod) { - // Rename "top" parameter to "maxCount" (with backward compatibility) - if (string.Equals(inputParam.Name, TopParameterName, StringComparison.OrdinalIgnoreCase)) + var backCompatTarget = client.BackCompatProvider; + + // Rename "top" parameter to "maxCount" (with backward compatibility). + // Use SerializedName (the original wire name) since Name may have been mutated previously. + if (string.Equals(inputParam.SerializedName, TopParameterName, StringComparison.OrdinalIgnoreCase)) { - UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, client); + UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, backCompatTarget); } // Ensure page size parameter uses the correct casing (with backward compatibility) - if (pageSizeParameterName != null && string.Equals(inputParam.Name, pageSizeParameterName, StringComparison.OrdinalIgnoreCase)) + if (pageSizeParameterName != null && string.Equals(inputParam.SerializedName, pageSizeParameterName, StringComparison.OrdinalIgnoreCase)) { var updatedPageSizeParameterName = pageSizeParameterName.Equals(MaxPageSizeParameterName, StringComparison.OrdinalIgnoreCase) ? MaxPageSizeParameterName : pageSizeParameterName; // For page size parameters, normalize badly-cased "maxpagesize" variants to proper camelCase, but always // respect backcompat. - UpdateParameterNameWithBackCompat(inputParam, updatedPageSizeParameterName, client); + UpdateParameterNameWithBackCompat(inputParam, updatedPageSizeParameterName, backCompatTarget); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/ListPageableTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/ListPageableTests.cs index b4d3827a2f9..2aaddae9e52 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/ListPageableTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/ListPageableTests.cs @@ -163,6 +163,100 @@ public void NoNextLinkOrContinuationTokenOfTAsync() Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + [Test] + public async Task TopParameterPreservedViaBackCompatProvider() + { + // This test verifies that when a different TypeProvider (e.g., MockableResourceProvider in mgmt) + // has "top" in its LastContractView, the GetConvenienceMethodByOperation method preserves the + // "top" parameter name even though the ClientProvider's own LastContractView doesn't have it. + var topParameter = InputFactory.QueryParameter("top", InputPrimitiveType.Int32, isRequired: false, serializedName: "top"); + + List parameters = [topParameter]; + List methodParameters = + [ + InputFactory.MethodParameter("top", InputPrimitiveType.Int32, isRequired: false, + location: InputRequestLocation.Query, serializedName: "top"), + ]; + + var inputModel = InputFactory.Model("Item", properties: + [ + InputFactory.Property("id", InputPrimitiveType.String, isRequired: true), + ]); + + var pagingMetadata = new InputPagingServiceMetadata( + ["items"], + new InputNextLink(null, ["nextLink"], InputResponseLocation.Body, []), + null, + null); + + var response = InputFactory.OperationResponse( + [200], + InputFactory.Model( + "PagedItems", + properties: [ + InputFactory.Property("items", InputFactory.Array(inputModel)), + InputFactory.Property("nextLink", InputPrimitiveType.Url) + ])); + + var operation = InputFactory.Operation("getItems", responses: [response], parameters: parameters); + var inputServiceMethod = InputFactory.PagingServiceMethod( + "getItems", + operation, + pagingMetadata: pagingMetadata, + parameters: methodParameters); + + var client = InputFactory.Client("testClient", methods: [inputServiceMethod]); + + var generator = await MockHelpers.LoadMockGeneratorAsync( + clients: () => [client], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType().FirstOrDefault(); + Assert.IsNotNull(clientProvider); + + // The ClientProvider's LastContractView should NOT have "top" (MockableTestResource has it, not TestClient) + // Verify the convenience method has maxCount (because the ClientProvider's own backcompat didn't find "top") + var methodsWithoutBackCompat = clientProvider!.GetMethodCollectionByOperation(operation); + var convenienceMethodWithoutBackCompat = methodsWithoutBackCompat[^2]; // sync convenience method + var maxCountParam = convenienceMethodWithoutBackCompat.Signature.Parameters.FirstOrDefault(p => + string.Equals(p.Name, "maxCount", StringComparison.Ordinal)); + Assert.IsNotNull(maxCountParam, "Without backcompat provider, parameter should be 'maxCount'"); + + // Now create a backcompat provider whose LastContractView has "top" + var backCompatProvider = new BackCompatTypeProvider("MockableTestResource", "Sample"); + Assert.IsNotNull(backCompatProvider.LastContractView, "BackCompat provider should have a LastContractView"); + + // Call GetMethodCollectionByOperation with the backcompat provider — this resets and rebuilds + var methodsWithBackCompat = clientProvider.GetMethodCollectionByOperation(operation, backCompatProvider); + var convenienceMethodWithBackCompat = methodsWithBackCompat[^2]; // sync convenience method + var topParam = convenienceMethodWithBackCompat.Signature.Parameters.FirstOrDefault(p => + string.Equals(p.Name, "top", StringComparison.Ordinal)); + + Assert.IsNotNull(topParam, "With backcompat provider, parameter should be 'top' (preserved from LastContractView)"); + Assert.AreEqual("top", topParam!.Name, + "Parameter name should be 'top' (from backcompat provider's LastContractView), not 'maxCount'"); + } + + /// + /// A simple TypeProvider used to simulate a backcompat provider (e.g., MockableResourceProvider) + /// whose LastContractView contains previously published parameter names. + /// + private class BackCompatTypeProvider : TypeProvider + { + private readonly string _name; + private readonly string _namespace; + + public BackCompatTypeProvider(string name, string ns) + { + _name = name; + _namespace = ns; + } + + protected override string BuildRelativeFilePath() => $"{_name}.cs"; + protected override string BuildName() => _name; + protected override string BuildNamespace() => _namespace; + } + private static void CreatePagingOperation() { var inputModel = InputFactory.Model("cat", properties: diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/ListPageableTests/TopParameterPreservedViaBackCompatProvider/MockableTestResource.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/ListPageableTests/TopParameterPreservedViaBackCompatProvider/MockableTestResource.cs new file mode 100644 index 00000000000..a16b13865c6 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/CollectionResultDefinitions/TestData/ListPageableTests/TopParameterPreservedViaBackCompatProvider/MockableTestResource.cs @@ -0,0 +1,18 @@ +#nullable disable + +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Threading.Tasks; + +namespace Sample +{ + /// + /// Represents the previous contract for the enclosing type (e.g., MockableResourceProvider) + /// that has the "top" parameter in its public API methods. + /// + public partial class MockableTestResource + { + public virtual Task GetItemsAsync(int? top, CancellationToken cancellationToken = default) { return null; } + public virtual ClientResult GetItems(int? top, CancellationToken cancellationToken = default) { return null; } + } +} From 81a6afb549b427f9fc248b1dbcd5d82aa78324d2 Mon Sep 17 00:00:00 2001 From: jolov Date: Fri, 13 Feb 2026 18:31:47 -0800 Subject: [PATCH 2/5] rename --- .../src/Providers/RestClientProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 2e9a5b85510..0d342165a48 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -1014,13 +1014,13 @@ internal static List GetMethodParameters( // For paging operations, handle parameter name corrections with backward compatibility if (serviceMethod is InputPagingServiceMethod) { - var backCompatTarget = client.BackCompatProvider; + var backCompatProvider = client.BackCompatProvider; // Rename "top" parameter to "maxCount" (with backward compatibility). // Use SerializedName (the original wire name) since Name may have been mutated previously. if (string.Equals(inputParam.SerializedName, TopParameterName, StringComparison.OrdinalIgnoreCase)) { - UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, backCompatTarget); + UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, backCompatProvider); } // Ensure page size parameter uses the correct casing (with backward compatibility) @@ -1031,7 +1031,7 @@ internal static List GetMethodParameters( : pageSizeParameterName; // For page size parameters, normalize badly-cased "maxpagesize" variants to proper camelCase, but always // respect backcompat. - UpdateParameterNameWithBackCompat(inputParam, updatedPageSizeParameterName, backCompatTarget); + UpdateParameterNameWithBackCompat(inputParam, updatedPageSizeParameterName, backCompatProvider); } } From 574a6157410abf9019e3749c86a6e1610e0d3e41 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 14 Feb 2026 18:36:33 -0800 Subject: [PATCH 3/5] toidentifier --- .../src/Providers/RestClientProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 0d342165a48..38ee3322248 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -882,7 +882,7 @@ private static void UpdateParameterNameWithBackCompat(InputParameter inputParame // by a previous call to this method. var existingParam = backCompatProvider.LastContractView?.Methods ?.SelectMany(method => method.Signature.Parameters) - .FirstOrDefault(p => string.Equals(p.Name, inputParameter.SerializedName, StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(p => string.Equals(p.Name, inputParameter.SerializedName.ToIdentifierName(), StringComparison.OrdinalIgnoreCase)) ?.Name; if (existingParam != null) From 41f4396e88e61336c23205948ca1453f325bfd0d Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 14 Feb 2026 18:59:14 -0800 Subject: [PATCH 4/5] additional --- .../src/Providers/RestClientProvider.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 38ee3322248..265252bca36 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -880,9 +880,10 @@ private static void UpdateParameterNameWithBackCompat(InputParameter inputParame // Check if the original wire name exists in LastContractView for backward compatibility. // We use SerializedName (the wire name) rather than Name because Name may have been mutated // by a previous call to this method. + var originalParameterName = inputParameter.SerializedName.ToIdentifierName(); var existingParam = backCompatProvider.LastContractView?.Methods ?.SelectMany(method => method.Signature.Parameters) - .FirstOrDefault(p => string.Equals(p.Name, inputParameter.SerializedName.ToIdentifierName(), StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(p => string.Equals(p.Name, originalParameterName, StringComparison.OrdinalIgnoreCase)) ?.Name; if (existingParam != null) @@ -1015,16 +1016,17 @@ internal static List GetMethodParameters( if (serviceMethod is InputPagingServiceMethod) { var backCompatProvider = client.BackCompatProvider; + var originalParameterName = inputParam.SerializedName.ToIdentifierName(); // Rename "top" parameter to "maxCount" (with backward compatibility). // Use SerializedName (the original wire name) since Name may have been mutated previously. - if (string.Equals(inputParam.SerializedName, TopParameterName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(originalParameterName, TopParameterName, StringComparison.OrdinalIgnoreCase)) { UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, backCompatProvider); } // Ensure page size parameter uses the correct casing (with backward compatibility) - if (pageSizeParameterName != null && string.Equals(inputParam.SerializedName, pageSizeParameterName, StringComparison.OrdinalIgnoreCase)) + if (pageSizeParameterName != null && string.Equals(originalParameterName, pageSizeParameterName, StringComparison.OrdinalIgnoreCase)) { var updatedPageSizeParameterName = pageSizeParameterName.Equals(MaxPageSizeParameterName, StringComparison.OrdinalIgnoreCase) ? MaxPageSizeParameterName From 22a8e92f51f896f399b1333481c296953cd01bf9 Mon Sep 17 00:00:00 2001 From: jolov Date: Sat, 14 Feb 2026 19:11:18 -0800 Subject: [PATCH 5/5] originalname --- .../src/Providers/RestClientProvider.cs | 13 ++++--------- .../src/InputTypes/InputParameter.cs | 6 ++++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 265252bca36..3802e185f95 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -878,12 +878,9 @@ private static bool TryGetSpecialHeaderParam(InputParameter inputParameter, [Not private static void UpdateParameterNameWithBackCompat(InputParameter inputParameter, string proposedName, TypeProvider backCompatProvider) { // Check if the original wire name exists in LastContractView for backward compatibility. - // We use SerializedName (the wire name) rather than Name because Name may have been mutated - // by a previous call to this method. - var originalParameterName = inputParameter.SerializedName.ToIdentifierName(); var existingParam = backCompatProvider.LastContractView?.Methods ?.SelectMany(method => method.Signature.Parameters) - .FirstOrDefault(p => string.Equals(p.Name, originalParameterName, StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(p => string.Equals(p.Name, inputParameter.OriginalName, StringComparison.OrdinalIgnoreCase)) ?.Name; if (existingParam != null) @@ -909,7 +906,7 @@ private static bool ShouldUpdateReinjectedParameter(InputParameter inputParamete // Check if this is a max page size parameter var pageSizeParameterName = GetPageSizeParameterName(pagingServiceMethod); - if (pageSizeParameterName != null && inputParameter.Name.Equals(pageSizeParameterName, StringComparison.OrdinalIgnoreCase)) + if (pageSizeParameterName != null && inputParameter.OriginalName.Equals(pageSizeParameterName, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -1016,17 +1013,15 @@ internal static List GetMethodParameters( if (serviceMethod is InputPagingServiceMethod) { var backCompatProvider = client.BackCompatProvider; - var originalParameterName = inputParam.SerializedName.ToIdentifierName(); // Rename "top" parameter to "maxCount" (with backward compatibility). - // Use SerializedName (the original wire name) since Name may have been mutated previously. - if (string.Equals(originalParameterName, TopParameterName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(inputParam.OriginalName, TopParameterName, StringComparison.OrdinalIgnoreCase)) { UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, backCompatProvider); } // Ensure page size parameter uses the correct casing (with backward compatibility) - if (pageSizeParameterName != null && string.Equals(originalParameterName, pageSizeParameterName, StringComparison.OrdinalIgnoreCase)) + if (pageSizeParameterName != null && string.Equals(inputParam.OriginalName, pageSizeParameterName, StringComparison.OrdinalIgnoreCase)) { var updatedPageSizeParameterName = pageSizeParameterName.Equals(MaxPageSizeParameterName, StringComparison.OrdinalIgnoreCase) ? MaxPageSizeParameterName diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputParameter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputParameter.cs index a19b88ecd90..9c5e665f02f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputParameter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputParameter.cs @@ -24,8 +24,14 @@ protected InputParameter( : base(name, summary, doc, type, isRequired, isReadOnly, access, serializedName, isApiVersion, defaultValue) { Scope = scope; + OriginalName = name; } + /// + /// Gets the original parameter name specified in the spec prior to any mutations. + /// + public string OriginalName { get; } + public InputParameterScope Scope { get; internal set; } public IReadOnlyList? MethodParameterSegments { get; internal set; }