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 0d5372e01ff..ae19e6f6f77 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 @@ -734,9 +734,11 @@ private MethodBodyStatement[] BuildPrimaryConstructorBody(IReadOnlyList Args) ConvertUriTemplateToFormattableString( + string uriTemplate, + IReadOnlyList parameters) + { + // Build a lookup for parameters by name (case-insensitive) + var paramsByName = new Dictionary(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(); + 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 GetSubClients() { var subClients = new List(_inputClient.Children.Count); 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 6f9878dd1b6..674bceafa4a 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 @@ -194,7 +194,7 @@ private MethodBodyStatements BuildMessage( var operation = serviceMethod.Operation; var classifier = GetClassifier(operation); - var paramMap = new Dictionary(signature.Parameters.ToDictionary(p => p.Name)); + var paramMap = new Dictionary(signature.Parameters.ToDictionary(p => p.Name), StringComparer.OrdinalIgnoreCase); foreach (var param in ClientProvider.ClientParameters) { paramMap[param.Name] = param; @@ -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; @@ -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(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs index 6068edcf87c..c774e76edf3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs @@ -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}\"")); + } } } + diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs index ba39f3a8488..f8030e64a61 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/RestClientProviders/RestClientProviderTests.cs @@ -1393,6 +1393,14 @@ private static IEnumerable 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( @@ -1420,7 +1428,7 @@ private static IEnumerable ValidateApiVersionPathParameterTestCase uri: "{endpoint}/{apiVersion}")) ], parameters: [endpointParameter, stringApiVersionParameter])); - + yield return new TestCaseData( InputFactory.Client( "TestClient", @@ -1433,6 +1441,45 @@ private static IEnumerable 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]