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..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 @@ -875,12 +875,12 @@ 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. + 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.OriginalName, StringComparison.OrdinalIgnoreCase)) ?.Name; if (existingParam != null) @@ -906,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; } @@ -1012,21 +1012,23 @@ 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 backCompatProvider = client.BackCompatProvider; + + // Rename "top" parameter to "maxCount" (with backward compatibility). + if (string.Equals(inputParam.OriginalName, TopParameterName, StringComparison.OrdinalIgnoreCase)) { - UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, client); + UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, backCompatProvider); } // 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.OriginalName, 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, backCompatProvider); } } 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; } + } +} 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; }