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>
/// Gets the effective type provider to use for backward compatibility checks.
/// When a <see cref="_backCompatProvider"/> is set, it is used instead of this client.
/// </summary>
internal TypeProvider BackCompatProvider => _backCompatProvider ?? this;

public ParameterProvider? ClientOptionsParameter { get; }

Expand Down Expand Up @@ -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];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
Expand Down Expand Up @@ -1012,21 +1012,23 @@ internal static List<ParameterProvider> 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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);

// 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'");
}

/// <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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ protected InputParameter(
: base(name, summary, doc, type, isRequired, isReadOnly, access, serializedName, isApiVersion, defaultValue)
{
Scope = scope;
OriginalName = name;
}

/// <summary>
/// Gets the original parameter name specified in the spec prior to any mutations.
/// </summary>
public string OriginalName { get; }

public InputParameterScope Scope { get; internal set; }
public IReadOnlyList<InputMethodParameter>? MethodParameterSegments { get; internal set; }

Expand Down