Skip to content
Merged
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 @@ -734,9 +734,11 @@ private MethodBodyStatement[] BuildPrimaryConstructorBody(IReadOnlyList<Paramete
if (_endpointParameter.Type.Equals(typeof(string)))
{
var serverTemplate = _inputEndpointParam!.ServerUrlTemplate;
// Build the URI by converting the named placeholders to indexed placeholders and collecting arguments
var (convertedTemplate, templateArgs) = ConvertUriTemplateToFormattableString(serverTemplate!, primaryConstructorParameters);
endpointAssignment = EndpointField.Assign(
New.Instance(typeof(Uri),
new FormattableStringExpression(serverTemplate!, [_endpointParameter])));
new FormattableStringExpression(convertedTemplate, templateArgs)));
}
else
{
Expand Down Expand Up @@ -1086,6 +1088,87 @@ private ParameterProvider BuildClientEndpointParameter()
};
}

/// Converts a URI template with named placeholders like "{Endpoint}/anomalydetector/{ApiVersion}"
/// to a formattable string format with indexed placeholders like "{0}/anomalydetector/{1}"
/// and returns the corresponding arguments.
private (string Template, List<ValueExpression> Args) ConvertUriTemplateToFormattableString(
string uriTemplate,
IReadOnlyList<ParameterProvider> parameters)
{
// Build a lookup for parameters by name (case-insensitive)
var paramsByName = new Dictionary<string, ParameterProvider>(StringComparer.OrdinalIgnoreCase);
foreach (var param in parameters)
{
paramsByName[param.Name] = param;
}

// Also add the endpoint parameter explicitly (it may have a different name)
if (!paramsByName.ContainsKey(_endpointParameter.Name))
{
paramsByName[_endpointParameter.Name] = _endpointParameter;
}

// Also add fields from _additionalClientFields
foreach (var field in _additionalClientFields.Value)
{
// Field names are like "_apiVersion", parameter names are like "ApiVersion"
var paramName = field.Name.TrimStart('_');
if (!paramsByName.ContainsKey(paramName))
{
paramsByName[paramName] = field.AsParameter;
}
}

var args = new List<ValueExpression>();
var result = new System.Text.StringBuilder();
var templateSpan = uriTemplate.AsSpan();

while (templateSpan.Length > 0)
{
var openBrace = templateSpan.IndexOf('{');
if (openBrace < 0)
{
// No more placeholders, append the rest
result.Append(templateSpan);
break;
}

// Append literal part before the placeholder
result.Append(templateSpan.Slice(0, openBrace));
templateSpan = templateSpan.Slice(openBrace + 1);

var closeBrace = templateSpan.IndexOf('}');
if (closeBrace < 0)
{
// Malformed template, append remaining as-is
result.Append('{');
result.Append(templateSpan);
break;
}

var paramName = templateSpan.Slice(0, closeBrace).ToString();
templateSpan = templateSpan.Slice(closeBrace + 1);

// Find the corresponding parameter or field
if (paramsByName.TryGetValue(paramName, out var param))
{
result.Append('{');
result.Append(args.Count);
result.Append('}');
args.Add(param.Field ?? (ValueExpression)param);
}
else
{
// Parameter not found - this is a configuration error
throw new InvalidOperationException(
$"URI template placeholder '{{{paramName}}}' in '{uriTemplate}' could not be resolved. " +
$"Available parameters: {string.Join(", ", paramsByName.Keys)}");
}
}

return (result.ToString(), args);
}

private IReadOnlyList<ClientProvider> GetSubClients()
{
var subClients = new List<ClientProvider>(_inputClient.Children.Count);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ private MethodBodyStatements BuildMessage(
var operation = serviceMethod.Operation;
var classifier = GetClassifier(operation);

var paramMap = new Dictionary<string, ParameterProvider>(signature.Parameters.ToDictionary(p => p.Name));
var paramMap = new Dictionary<string, ParameterProvider>(signature.Parameters.ToDictionary(p => p.Name), StringComparer.OrdinalIgnoreCase);
foreach (var param in ClientProvider.ClientParameters)
{
paramMap[param.Name] = param;
Expand Down Expand Up @@ -703,7 +703,7 @@ private void AddUriSegments(
/* when the parameter is in operation.uri, it is client parameter
* It is not operation parameter and not in inputParamHash list.
*/
var isClientParameter = ClientProvider.ClientParameters.Any(p => p.Name == paramName);
var isClientParameter = ClientProvider.ClientParameters.Any(p => string.Equals(p.Name, paramName, StringComparison.OrdinalIgnoreCase));
CSharpType? type;
SerializationFormat? serializationFormat;
ValueExpression? valueExpression;
Expand All @@ -714,25 +714,18 @@ private void AddUriSegments(
}
else
{
if (isClientParameter)
inputParam = inputParamMap[paramName];
if (inputParam is InputPathParameter || inputParam is InputEndpointParameter)
{
GetParamInfo(paramMap[paramName], out type, out serializationFormat, out valueExpression);
GetParamInfo(paramMap, operation, inputParam, out type, out serializationFormat, out valueExpression);
if (valueExpression == null)
{
break;
}
}
else
{
inputParam = inputParamMap[paramName];
if (inputParam is InputPathParameter || inputParam is InputEndpointParameter)
{
GetParamInfo(paramMap, operation, inputParam, out type, out serializationFormat, out valueExpression);
if (valueExpression == null)
{
break;
}
}
else
{
throw new InvalidOperationException($"The location of parameter {inputParam.Name} should be path or uri");
}
throw new InvalidOperationException($"The location of parameter {inputParam.Name} should be path or uri");
}
}
string? format = serializationFormat?.ToFormatSpecifier();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3427,5 +3427,108 @@ public void GetApiVersionFieldForService_MultiService_CaseInsensitiveMatch()
Assert.IsNotNull(fieldUpperCase);
Assert.AreEqual("_serviceAApiVersion", fieldUpperCase!.Name);
}

[TestCase("{endpoint}")]
[TestCase("{Endpoint}")]
[TestCase("{ENDPOINT}")]
public void ConvertUriTemplate_CaseInsensitiveEndpointLookup(string serverTemplate)
{
// Tests that the parameter lookup in ConvertUriTemplateToFormattableString is case-insensitive
MockHelpers.LoadMockGenerator();
var client = InputFactory.Client(
TestClientName,
parameters: [InputFactory.EndpointParameter(
"endpoint",
InputPrimitiveType.String,
isRequired: true,
scope: InputParameterScope.Client,
serverUrlTemplate: serverTemplate,
isEndpoint: true)]);
var clientProvider = new ClientProvider(client);
var constructor = clientProvider.Constructors.FirstOrDefault(
c => c.Signature.Initializer == null && c.Signature?.Modifiers == MethodSignatureModifiers.Public);

Assert.IsNotNull(constructor);
// Should not throw and should contain the Uri assignment
var bodyText = constructor!.BodyStatements!.ToDisplayString();
Assert.IsTrue(bodyText.Contains("_endpoint = new global::System.Uri($\""));
}

[Test]
public void ConvertUriTemplate_CaseInsensitivePathParameterLookup()
{
// Tests template with mixed case placeholders like "{Endpoint}/services/{ApiVersion}"
MockHelpers.LoadMockGenerator();

var serverTemplate = "{Endpoint}/{ApiVersion}";
var client = InputFactory.Client(
TestClientName,
methods: [InputFactory.BasicServiceMethod("Test", InputFactory.Operation("test", uri: serverTemplate))],
parameters: [
InputFactory.EndpointParameter(
"endpoint", // lowercase parameter name
InputPrimitiveType.String,
isRequired: true,
scope: InputParameterScope.Client,
serverUrlTemplate: serverTemplate,
isEndpoint: true),
InputFactory.PathParameter(
"apiVersion", // lowercase parameter name
InputPrimitiveType.String,
isRequired: true,
scope: InputParameterScope.Client)
]);
var clientProvider = new ClientProvider(client);
var constructor = clientProvider.Constructors.FirstOrDefault(
c => c.Signature.Initializer == null && c.Signature?.Modifiers == MethodSignatureModifiers.Public);

Assert.IsNotNull(constructor);
// Should not throw - case-insensitive lookup should find parameters
var bodyText = constructor!.BodyStatements!.ToDisplayString();
Assert.IsNotNull(bodyText);
// Verify that the Uri is built according to the server template with case-insensitive parameter matching
Assert.IsTrue(bodyText.Contains("$\"{endpoint}/{_apiVersion}\""));
}

[Test]
public void ConvertUriTemplate_WithMultiplePlaceholders()
{
// Tests template with multiple placeholders: "{endpoint}/{apiVersion}/services/{subscriptionId}"
MockHelpers.LoadMockGenerator();

var serverTemplate = "{endpoint}/{apiVersion}/services/{subscriptionId}";
var client = InputFactory.Client(
TestClientName,
methods: [InputFactory.BasicServiceMethod("Test", InputFactory.Operation("test", uri: serverTemplate))],
parameters: [
InputFactory.EndpointParameter(
"endpoint",
InputPrimitiveType.String,
isRequired: true,
scope: InputParameterScope.Client,
serverUrlTemplate: serverTemplate,
isEndpoint: true),
InputFactory.PathParameter(
"apiVersion",
InputPrimitiveType.String,
isRequired: true,
scope: InputParameterScope.Client),
InputFactory.PathParameter(
"subscriptionId",
InputPrimitiveType.String,
isRequired: true,
scope: InputParameterScope.Client)
]);
var clientProvider = new ClientProvider(client);
var constructor = clientProvider.Constructors.FirstOrDefault(
c => c.Signature.Initializer == null && c.Signature?.Modifiers == MethodSignatureModifiers.Public);

Assert.IsNotNull(constructor);
var bodyText = constructor!.BodyStatements!.ToDisplayString();
Assert.IsNotNull(bodyText);
// Verify that the Uri is built according to the server template
Assert.IsTrue(bodyText.Contains("$\"{endpoint}/{_apiVersion}/services/{_subscriptionId}\""));
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,14 @@ private static IEnumerable<TestCaseData> ValidateApiVersionPathParameterTestCase
scope: InputParameterScope.Client,
isApiVersion: true);

InputMethodParameter pascalCaseApiVersionParameter = InputFactory.MethodParameter(
"ApiVersion",
InputPrimitiveType.String,
location: InputRequestLocation.Uri,
isRequired: true,
scope: InputParameterScope.Client,
isApiVersion: true);

InputMethodParameter enumApiVersionParameter = InputFactory.MethodParameter(
"apiVersion",
InputFactory.StringEnum(
Expand Down Expand Up @@ -1420,7 +1428,7 @@ private static IEnumerable<TestCaseData> ValidateApiVersionPathParameterTestCase
uri: "{endpoint}/{apiVersion}"))
],
parameters: [endpointParameter, stringApiVersionParameter]));

yield return new TestCaseData(
InputFactory.Client(
"TestClient",
Expand All @@ -1433,6 +1441,45 @@ private static IEnumerable<TestCaseData> ValidateApiVersionPathParameterTestCase
uri: "{endpoint}/{apiVersion}"))
],
parameters: [endpointParameter, enumApiVersionParameter]));

yield return new TestCaseData(
InputFactory.Client(
"TestClient",
methods:
[
InputFactory.BasicServiceMethod(
"TestServiceMethod",
InputFactory.Operation(
"TestOperation",
uri: "{endpoint}/{ApiVersion}"))
],
parameters: [endpointParameter, stringApiVersionParameter]));

yield return new TestCaseData(
InputFactory.Client(
"TestClient",
methods:
[
InputFactory.BasicServiceMethod(
"TestServiceMethod",
InputFactory.Operation(
"TestOperation",
uri: "{endpoint}/{apiVersion}"))
],
parameters: [endpointParameter, pascalCaseApiVersionParameter]));

yield return new TestCaseData(
InputFactory.Client(
"TestClient",
methods:
[
InputFactory.BasicServiceMethod(
"TestServiceMethod",
InputFactory.Operation(
"TestOperation",
uri: "{endpoint}/{ApiVersion}"))
],
parameters: [endpointParameter, pascalCaseApiVersionParameter]));
}

[Test]
Expand Down