Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ private record ApiVersionFields(FieldProvider Field, PropertyProvider? Correspon

private Dictionary<InputOperation, ScmMethodProviderCollection>? _methodCache;
private Dictionary<InputOperation, ScmMethodProviderCollection> MethodCache => _methodCache ??= [];
private TypeProvider? _backCompatProvider;

/// <summary>
/// When set, this provider's <see cref="TypeProvider.LastContractView"/> is used for backward
/// compatibility checks on paging parameter names instead of this client's own LastContractView.
/// </summary>
internal TypeProvider? BackCompatProvider => _backCompatProvider;

public ParameterProvider? ClientOptionsParameter { get; }

Expand Down Expand Up @@ -835,8 +842,19 @@ [.. primaryCtorOrderedParams.Select(p => secondaryParamNames.Contains(p.Name) ?
this);
}

public ScmMethodProviderCollection GetMethodCollectionByOperation(InputOperation operation)
public ScmMethodProviderCollection GetMethodCollectionByOperation(InputOperation operation, TypeProvider? backCompatProvider = null)
{
// Normalize: passing `this` is the same as no override.
var effective = backCompatProvider != null && backCompatProvider != this ? backCompatProvider : null;
if (_backCompatProvider != effective)
{
_backCompatProvider = effective;
// Clear caches so methods are rebuilt with the new backcompat provider.
// Since InputParameter objects are not mutated, Reset() is safe to call
// repeatedly — the rebuild will produce correct results each time.
Reset();
_methodCache = null;
}
_ = Methods; // Ensure methods are built
return MethodCache[operation];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -875,24 +875,30 @@ private static bool TryGetSpecialHeaderParam(InputParameter inputParameter, [Not
: null;
}

private static void UpdateParameterNameWithBackCompat(InputParameter inputParameter, string proposedName, ClientProvider client)
private static void UpdateParameterNameWithBackCompat(ParameterProvider parameter, string proposedName, TypeProvider backCompatProvider)
{
// Check if the original parameter name exists in LastContractView for backward compatibility
var existingParam = client.LastContractView?.Methods
?.SelectMany(method => method.Signature.Parameters)
.FirstOrDefault(p => string.Equals(p.Name, inputParameter.Name, StringComparison.OrdinalIgnoreCase))
?.Name;

if (existingParam != null)
// Check if the original wire name exists in LastContractView for backward compatibility.
// We use WireInfo.SerializedName (the original wire name) to look up the parameter in the
// previous contract, since this is stable and never mutated.
var serializedName = parameter.WireInfo?.SerializedName;
if (serializedName != null)
{
// Preserve the exact name (including casing) from the previous contract for backward compatibility
proposedName = existingParam;
var existingParamName = backCompatProvider.LastContractView?.Methods
?.SelectMany(method => method.Signature.Parameters)
.FirstOrDefault(p => string.Equals(p.Name, serializedName, StringComparison.OrdinalIgnoreCase))
?.Name;

if (existingParamName != null)
{
// Preserve the exact name (including casing) from the previous contract for backward compatibility
proposedName = existingParamName;
}
}

// Use the updated name
if (!string.Equals(inputParameter.Name, proposedName, StringComparison.Ordinal))
// Apply the updated name to the ParameterProvider (not the InputParameter)
if (!string.Equals(parameter.Name, proposedName, StringComparison.Ordinal))
{
inputParameter.Update(name: proposedName);
parameter.Update(name: proposedName);
}
}

Expand Down Expand Up @@ -949,7 +955,8 @@ public MethodProvider GetCreateNextLinkRequestMethod(InputOperation operation)
internal static List<ParameterProvider> GetMethodParameters(
InputServiceMethod serviceMethod,
ScmMethodKind methodType,
ClientProvider client)
ClientProvider client,
TypeProvider? backCompatProvider = null)
{
SortedList<int, ParameterProvider> sortedParams = [];
int path = 0;
Expand Down Expand Up @@ -1009,33 +1016,35 @@ internal static List<ParameterProvider> GetMethodParameters(
continue;
}

// For paging operations, handle parameter name corrections with backward compatibility
ParameterProvider? parameter = ScmCodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParam)?.ToPublicInputParameter();
if (parameter is null)
{
continue;
}

// For paging operations, handle parameter name corrections with backward compatibility.
// This is done after ParameterProvider creation so we don't mutate shared InputParameter objects.
if (serviceMethod is InputPagingServiceMethod)
{
// Rename "top" parameter to "maxCount" (with backward compatibility)
if (string.Equals(inputParam.Name, TopParameterName, StringComparison.OrdinalIgnoreCase))
var backCompatTarget = backCompatProvider ?? client;

// Rename "top" parameter to "maxCount" (with backward compatibility).
// Use SerializedName (the original wire name) which is stable and never mutated.
if (string.Equals(inputParam.SerializedName, TopParameterName, StringComparison.OrdinalIgnoreCase))
{
UpdateParameterNameWithBackCompat(inputParam, MaxCountParameterName, client);
UpdateParameterNameWithBackCompat(parameter, 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(parameter, updatedPageSizeParameterName, backCompatTarget);
}
}

ParameterProvider? parameter = ScmCodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParam)?.ToPublicInputParameter();
if (parameter is null)
{
continue;
}

if (methodType is ScmMethodKind.Protocol or ScmMethodKind.CreateRequest)
{
if (inputParam is InputBodyParameter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class ScmMethodProviderCollection : IReadOnlyList<ScmMethodProvider>
private IList<ParameterProvider> ProtocolMethodParameters => _protocolMethodParameters ??= RestClientProvider.GetMethodParameters(ServiceMethod, ScmMethodKind.Protocol, Client);
private IList<ParameterProvider>? _protocolMethodParameters;

private IReadOnlyList<ParameterProvider> ConvenienceMethodParameters => _convenienceMethodParameters ??= RestClientProvider.GetMethodParameters(ServiceMethod, ScmMethodKind.Convenience, Client);
private IReadOnlyList<ParameterProvider> ConvenienceMethodParameters => _convenienceMethodParameters ??= RestClientProvider.GetMethodParameters(ServiceMethod, ScmMethodKind.Convenience, Client, Client.BackCompatProvider);
private IReadOnlyList<ParameterProvider>? _convenienceMethodParameters;
private readonly InputPagingServiceMethod? _pagingServiceMethod;
private IReadOnlyList<ScmMethodProvider>? _methods;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,98 @@ 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)
// is passed as a backCompatProvider and has "top" in its LastContractView, the parameter name
// is preserved as "top" instead of being renamed to "maxCount".
var topParameter = InputFactory.QueryParameter("top", InputPrimitiveType.Int32, isRequired: false, serializedName: "top");

List<InputParameter> parameters = [topParameter];
List<InputMethodParameter> 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<ClientProvider>().FirstOrDefault();
Assert.IsNotNull(clientProvider);

// Without a backcompat provider, the parameter should be renamed to "maxCount"
var methodsWithoutBackCompat = clientProvider!.GetMethodCollectionByOperation(operation);
var convenienceMethod = methodsWithoutBackCompat[^2]; // sync convenience method
var maxCountParam = convenienceMethod.Signature.Parameters.FirstOrDefault(p =>
string.Equals(p.Name, "maxCount", StringComparison.Ordinal));
Assert.IsNotNull(maxCountParam, "Without backcompat provider, parameter should be 'maxCount'");

// With a backcompat provider whose LastContractView has "top", the name should be preserved
var backCompatProvider = new BackCompatTypeProvider("MockableTestResource", "Sample");
Assert.IsNotNull(backCompatProvider.LastContractView, "BackCompat provider should have a LastContractView");

var methodsWithBackCompat = clientProvider.GetMethodCollectionByOperation(operation, backCompatProvider);
convenienceMethod = methodsWithBackCompat[^2];
var topParam = convenienceMethod.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'");
}

/// <summary>
/// A simple TypeProvider used to simulate a backcompat provider (e.g., MockableResourceProvider)
/// whose LastContractView contains previously published parameter names.
/// </summary>
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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#nullable disable

using System.ClientModel;
using System.ClientModel.Primitives;
using System.Threading.Tasks;

namespace Sample
{
/// <summary>
/// Represents the previous contract for the enclosing type (e.g., MockableResourceProvider)
/// that has the "top" parameter in its public API methods.
/// </summary>
public partial class MockableTestResource
{
public virtual Task<ClientResult> GetItemsAsync(int? top, CancellationToken cancellationToken = default) { return null; }
public virtual ClientResult GetItems(int? top, CancellationToken cancellationToken = default) { return null; }
}
}