Skip to content

Commit c6f1f96

Browse files
MaiLinhPLinh Phan
andauthored
Generator fix to handle apiVersion casing and multiple placeholders in URI path (#9561)
Make paramMap look up case insensitive to avoid cases like ```"ApiVersion"``` in server parameters vs ```"apiVersion"```. Fix template for URI path to be compatible with multiple placeholders like ```"{Endpoint}/anomalydetector/{ApiVersion}"```. --------- Co-authored-by: Linh Phan <mailinhphan@microsoft.com>
1 parent 9e075f0 commit c6f1f96

File tree

4 files changed

+245
-19
lines changed

4 files changed

+245
-19
lines changed

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,9 +734,11 @@ private MethodBodyStatement[] BuildPrimaryConstructorBody(IReadOnlyList<Paramete
734734
if (_endpointParameter.Type.Equals(typeof(string)))
735735
{
736736
var serverTemplate = _inputEndpointParam!.ServerUrlTemplate;
737+
// Build the URI by converting the named placeholders to indexed placeholders and collecting arguments
738+
var (convertedTemplate, templateArgs) = ConvertUriTemplateToFormattableString(serverTemplate!, primaryConstructorParameters);
737739
endpointAssignment = EndpointField.Assign(
738740
New.Instance(typeof(Uri),
739-
new FormattableStringExpression(serverTemplate!, [_endpointParameter])));
741+
new FormattableStringExpression(convertedTemplate, templateArgs)));
740742
}
741743
else
742744
{
@@ -1086,6 +1088,87 @@ private ParameterProvider BuildClientEndpointParameter()
10861088
};
10871089
}
10881090

1091+
/// Converts a URI template with named placeholders like "{Endpoint}/anomalydetector/{ApiVersion}"
1092+
/// to a formattable string format with indexed placeholders like "{0}/anomalydetector/{1}"
1093+
/// and returns the corresponding arguments.
1094+
private (string Template, List<ValueExpression> Args) ConvertUriTemplateToFormattableString(
1095+
string uriTemplate,
1096+
IReadOnlyList<ParameterProvider> parameters)
1097+
{
1098+
// Build a lookup for parameters by name (case-insensitive)
1099+
var paramsByName = new Dictionary<string, ParameterProvider>(StringComparer.OrdinalIgnoreCase);
1100+
foreach (var param in parameters)
1101+
{
1102+
paramsByName[param.Name] = param;
1103+
}
1104+
1105+
// Also add the endpoint parameter explicitly (it may have a different name)
1106+
if (!paramsByName.ContainsKey(_endpointParameter.Name))
1107+
{
1108+
paramsByName[_endpointParameter.Name] = _endpointParameter;
1109+
}
1110+
1111+
// Also add fields from _additionalClientFields
1112+
foreach (var field in _additionalClientFields.Value)
1113+
{
1114+
// Field names are like "_apiVersion", parameter names are like "ApiVersion"
1115+
var paramName = field.Name.TrimStart('_');
1116+
if (!paramsByName.ContainsKey(paramName))
1117+
{
1118+
paramsByName[paramName] = field.AsParameter;
1119+
}
1120+
}
1121+
1122+
var args = new List<ValueExpression>();
1123+
var result = new System.Text.StringBuilder();
1124+
var templateSpan = uriTemplate.AsSpan();
1125+
1126+
while (templateSpan.Length > 0)
1127+
{
1128+
var openBrace = templateSpan.IndexOf('{');
1129+
if (openBrace < 0)
1130+
{
1131+
// No more placeholders, append the rest
1132+
result.Append(templateSpan);
1133+
break;
1134+
}
1135+
1136+
// Append literal part before the placeholder
1137+
result.Append(templateSpan.Slice(0, openBrace));
1138+
templateSpan = templateSpan.Slice(openBrace + 1);
1139+
1140+
var closeBrace = templateSpan.IndexOf('}');
1141+
if (closeBrace < 0)
1142+
{
1143+
// Malformed template, append remaining as-is
1144+
result.Append('{');
1145+
result.Append(templateSpan);
1146+
break;
1147+
}
1148+
1149+
var paramName = templateSpan.Slice(0, closeBrace).ToString();
1150+
templateSpan = templateSpan.Slice(closeBrace + 1);
1151+
1152+
// Find the corresponding parameter or field
1153+
if (paramsByName.TryGetValue(paramName, out var param))
1154+
{
1155+
result.Append('{');
1156+
result.Append(args.Count);
1157+
result.Append('}');
1158+
args.Add(param.Field ?? (ValueExpression)param);
1159+
}
1160+
else
1161+
{
1162+
// Parameter not found - this is a configuration error
1163+
throw new InvalidOperationException(
1164+
$"URI template placeholder '{{{paramName}}}' in '{uriTemplate}' could not be resolved. " +
1165+
$"Available parameters: {string.Join(", ", paramsByName.Keys)}");
1166+
}
1167+
}
1168+
1169+
return (result.ToString(), args);
1170+
}
1171+
10891172
private IReadOnlyList<ClientProvider> GetSubClients()
10901173
{
10911174
var subClients = new List<ClientProvider>(_inputClient.Children.Count);

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ private MethodBodyStatements BuildMessage(
194194
var operation = serviceMethod.Operation;
195195
var classifier = GetClassifier(operation);
196196

197-
var paramMap = new Dictionary<string, ParameterProvider>(signature.Parameters.ToDictionary(p => p.Name));
197+
var paramMap = new Dictionary<string, ParameterProvider>(signature.Parameters.ToDictionary(p => p.Name), StringComparer.OrdinalIgnoreCase);
198198
foreach (var param in ClientProvider.ClientParameters)
199199
{
200200
paramMap[param.Name] = param;
@@ -703,7 +703,7 @@ private void AddUriSegments(
703703
/* when the parameter is in operation.uri, it is client parameter
704704
* It is not operation parameter and not in inputParamHash list.
705705
*/
706-
var isClientParameter = ClientProvider.ClientParameters.Any(p => p.Name == paramName);
706+
var isClientParameter = ClientProvider.ClientParameters.Any(p => string.Equals(p.Name, paramName, StringComparison.OrdinalIgnoreCase));
707707
CSharpType? type;
708708
SerializationFormat? serializationFormat;
709709
ValueExpression? valueExpression;
@@ -714,25 +714,18 @@ private void AddUriSegments(
714714
}
715715
else
716716
{
717-
if (isClientParameter)
717+
inputParam = inputParamMap[paramName];
718+
if (inputParam is InputPathParameter || inputParam is InputEndpointParameter)
718719
{
719-
GetParamInfo(paramMap[paramName], out type, out serializationFormat, out valueExpression);
720+
GetParamInfo(paramMap, operation, inputParam, out type, out serializationFormat, out valueExpression);
721+
if (valueExpression == null)
722+
{
723+
break;
724+
}
720725
}
721726
else
722727
{
723-
inputParam = inputParamMap[paramName];
724-
if (inputParam is InputPathParameter || inputParam is InputEndpointParameter)
725-
{
726-
GetParamInfo(paramMap, operation, inputParam, out type, out serializationFormat, out valueExpression);
727-
if (valueExpression == null)
728-
{
729-
break;
730-
}
731-
}
732-
else
733-
{
734-
throw new InvalidOperationException($"The location of parameter {inputParam.Name} should be path or uri");
735-
}
728+
throw new InvalidOperationException($"The location of parameter {inputParam.Name} should be path or uri");
736729
}
737730
}
738731
string? format = serializationFormat?.ToFormatSpecifier();

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3427,5 +3427,108 @@ public void GetApiVersionFieldForService_MultiService_CaseInsensitiveMatch()
34273427
Assert.IsNotNull(fieldUpperCase);
34283428
Assert.AreEqual("_serviceAApiVersion", fieldUpperCase!.Name);
34293429
}
3430+
3431+
[TestCase("{endpoint}")]
3432+
[TestCase("{Endpoint}")]
3433+
[TestCase("{ENDPOINT}")]
3434+
public void ConvertUriTemplate_CaseInsensitiveEndpointLookup(string serverTemplate)
3435+
{
3436+
// Tests that the parameter lookup in ConvertUriTemplateToFormattableString is case-insensitive
3437+
MockHelpers.LoadMockGenerator();
3438+
var client = InputFactory.Client(
3439+
TestClientName,
3440+
parameters: [InputFactory.EndpointParameter(
3441+
"endpoint",
3442+
InputPrimitiveType.String,
3443+
isRequired: true,
3444+
scope: InputParameterScope.Client,
3445+
serverUrlTemplate: serverTemplate,
3446+
isEndpoint: true)]);
3447+
var clientProvider = new ClientProvider(client);
3448+
var constructor = clientProvider.Constructors.FirstOrDefault(
3449+
c => c.Signature.Initializer == null && c.Signature?.Modifiers == MethodSignatureModifiers.Public);
3450+
3451+
Assert.IsNotNull(constructor);
3452+
// Should not throw and should contain the Uri assignment
3453+
var bodyText = constructor!.BodyStatements!.ToDisplayString();
3454+
Assert.IsTrue(bodyText.Contains("_endpoint = new global::System.Uri($\""));
3455+
}
3456+
3457+
[Test]
3458+
public void ConvertUriTemplate_CaseInsensitivePathParameterLookup()
3459+
{
3460+
// Tests template with mixed case placeholders like "{Endpoint}/services/{ApiVersion}"
3461+
MockHelpers.LoadMockGenerator();
3462+
3463+
var serverTemplate = "{Endpoint}/{ApiVersion}";
3464+
var client = InputFactory.Client(
3465+
TestClientName,
3466+
methods: [InputFactory.BasicServiceMethod("Test", InputFactory.Operation("test", uri: serverTemplate))],
3467+
parameters: [
3468+
InputFactory.EndpointParameter(
3469+
"endpoint", // lowercase parameter name
3470+
InputPrimitiveType.String,
3471+
isRequired: true,
3472+
scope: InputParameterScope.Client,
3473+
serverUrlTemplate: serverTemplate,
3474+
isEndpoint: true),
3475+
InputFactory.PathParameter(
3476+
"apiVersion", // lowercase parameter name
3477+
InputPrimitiveType.String,
3478+
isRequired: true,
3479+
scope: InputParameterScope.Client)
3480+
]);
3481+
var clientProvider = new ClientProvider(client);
3482+
var constructor = clientProvider.Constructors.FirstOrDefault(
3483+
c => c.Signature.Initializer == null && c.Signature?.Modifiers == MethodSignatureModifiers.Public);
3484+
3485+
Assert.IsNotNull(constructor);
3486+
// Should not throw - case-insensitive lookup should find parameters
3487+
var bodyText = constructor!.BodyStatements!.ToDisplayString();
3488+
Assert.IsNotNull(bodyText);
3489+
// Verify that the Uri is built according to the server template with case-insensitive parameter matching
3490+
Assert.IsTrue(bodyText.Contains("$\"{endpoint}/{_apiVersion}\""));
3491+
}
3492+
3493+
[Test]
3494+
public void ConvertUriTemplate_WithMultiplePlaceholders()
3495+
{
3496+
// Tests template with multiple placeholders: "{endpoint}/{apiVersion}/services/{subscriptionId}"
3497+
MockHelpers.LoadMockGenerator();
3498+
3499+
var serverTemplate = "{endpoint}/{apiVersion}/services/{subscriptionId}";
3500+
var client = InputFactory.Client(
3501+
TestClientName,
3502+
methods: [InputFactory.BasicServiceMethod("Test", InputFactory.Operation("test", uri: serverTemplate))],
3503+
parameters: [
3504+
InputFactory.EndpointParameter(
3505+
"endpoint",
3506+
InputPrimitiveType.String,
3507+
isRequired: true,
3508+
scope: InputParameterScope.Client,
3509+
serverUrlTemplate: serverTemplate,
3510+
isEndpoint: true),
3511+
InputFactory.PathParameter(
3512+
"apiVersion",
3513+
InputPrimitiveType.String,
3514+
isRequired: true,
3515+
scope: InputParameterScope.Client),
3516+
InputFactory.PathParameter(
3517+
"subscriptionId",
3518+
InputPrimitiveType.String,
3519+
isRequired: true,
3520+
scope: InputParameterScope.Client)
3521+
]);
3522+
var clientProvider = new ClientProvider(client);
3523+
var constructor = clientProvider.Constructors.FirstOrDefault(
3524+
c => c.Signature.Initializer == null && c.Signature?.Modifiers == MethodSignatureModifiers.Public);
3525+
3526+
Assert.IsNotNull(constructor);
3527+
var bodyText = constructor!.BodyStatements!.ToDisplayString();
3528+
Assert.IsNotNull(bodyText);
3529+
// Verify that the Uri is built according to the server template
3530+
Assert.IsTrue(bodyText.Contains("$\"{endpoint}/{_apiVersion}/services/{_subscriptionId}\""));
3531+
}
34303532
}
34313533
}
3534+

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,14 @@ private static IEnumerable<TestCaseData> ValidateApiVersionPathParameterTestCase
13931393
scope: InputParameterScope.Client,
13941394
isApiVersion: true);
13951395

1396+
InputMethodParameter pascalCaseApiVersionParameter = InputFactory.MethodParameter(
1397+
"ApiVersion",
1398+
InputPrimitiveType.String,
1399+
location: InputRequestLocation.Uri,
1400+
isRequired: true,
1401+
scope: InputParameterScope.Client,
1402+
isApiVersion: true);
1403+
13961404
InputMethodParameter enumApiVersionParameter = InputFactory.MethodParameter(
13971405
"apiVersion",
13981406
InputFactory.StringEnum(
@@ -1420,7 +1428,7 @@ private static IEnumerable<TestCaseData> ValidateApiVersionPathParameterTestCase
14201428
uri: "{endpoint}/{apiVersion}"))
14211429
],
14221430
parameters: [endpointParameter, stringApiVersionParameter]));
1423-
1431+
14241432
yield return new TestCaseData(
14251433
InputFactory.Client(
14261434
"TestClient",
@@ -1433,6 +1441,45 @@ private static IEnumerable<TestCaseData> ValidateApiVersionPathParameterTestCase
14331441
uri: "{endpoint}/{apiVersion}"))
14341442
],
14351443
parameters: [endpointParameter, enumApiVersionParameter]));
1444+
1445+
yield return new TestCaseData(
1446+
InputFactory.Client(
1447+
"TestClient",
1448+
methods:
1449+
[
1450+
InputFactory.BasicServiceMethod(
1451+
"TestServiceMethod",
1452+
InputFactory.Operation(
1453+
"TestOperation",
1454+
uri: "{endpoint}/{ApiVersion}"))
1455+
],
1456+
parameters: [endpointParameter, stringApiVersionParameter]));
1457+
1458+
yield return new TestCaseData(
1459+
InputFactory.Client(
1460+
"TestClient",
1461+
methods:
1462+
[
1463+
InputFactory.BasicServiceMethod(
1464+
"TestServiceMethod",
1465+
InputFactory.Operation(
1466+
"TestOperation",
1467+
uri: "{endpoint}/{apiVersion}"))
1468+
],
1469+
parameters: [endpointParameter, pascalCaseApiVersionParameter]));
1470+
1471+
yield return new TestCaseData(
1472+
InputFactory.Client(
1473+
"TestClient",
1474+
methods:
1475+
[
1476+
InputFactory.BasicServiceMethod(
1477+
"TestServiceMethod",
1478+
InputFactory.Operation(
1479+
"TestOperation",
1480+
uri: "{endpoint}/{ApiVersion}"))
1481+
],
1482+
parameters: [endpointParameter, pascalCaseApiVersionParameter]));
14361483
}
14371484

14381485
[Test]

0 commit comments

Comments
 (0)