diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs index af0741751ea..5f7e3d38390 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientSettingsProvider.cs @@ -163,13 +163,13 @@ protected override MethodProvider[] BuildMethods() if (EndpointProperty != null) { - AppendBindingForProperty(body, sectionParam, EndpointProperty.Name, EndpointProperty.Name.ToVariableName(), EndpointProperty.Type); + AppendBindingForProperty(body, sectionParam, GetSettingPropertyName(EndpointProperty.Name), EndpointProperty.Name.ToVariableName(), EndpointProperty.Type, EndpointProperty.Name); } foreach (var param in OtherRequiredParams) { var propName = param.Name.ToIdentifierName(); - AppendBindingForProperty(body, sectionParam, propName, param.Name.ToVariableName(), param.Type); + AppendBindingForProperty(body, sectionParam, GetSettingPropertyName(propName), param.Name.ToVariableName(), param.Type, propName); } // Bind custom constructor parameters from custom code. @@ -212,7 +212,7 @@ protected override MethodProvider[] BuildMethods() var clientOptions = _clientProvider.EffectiveClientOptions; if (clientOptions != null) { - AppendComplexObjectBinding(body, sectionParam, "Options", "options", clientOptions.Type); + AppendComplexObjectBinding(body, sectionParam, GetSettingPropertyName("Options"), "options", clientOptions.Type, "Options"); } var bindCoreMethod = new MethodProvider( @@ -229,6 +229,24 @@ protected override MethodProvider[] BuildMethods() return [bindCoreMethod]; } + /// + /// Resolves the effective property name to assign to during binding, honoring custom code + /// replacements (e.g., a property renamed via [CodeGenMember("OriginalName")]). + /// The configuration key used to read the value remains the original generated name. + /// + private string GetSettingPropertyName(string generatedName) + { + foreach (var property in CanonicalView.Properties) + { + if (property.OriginalName == generatedName) + { + return property.Name; + } + } + + return generatedName; + } + /// /// Dispatches to the appropriate binding method based on the property type. /// @@ -237,8 +255,11 @@ internal static void AppendBindingForProperty( ParameterProvider sectionParam, string propName, string varName, - CSharpType type) + CSharpType type, + string? configKey = null) { + configKey ??= propName; + // Handle non-framework types (enums, complex objects) if (!type.IsFrameworkType) { @@ -246,11 +267,11 @@ internal static void AppendBindingForProperty( { if (type.IsStruct) { - AppendEnumBinding(body, sectionParam, propName, varName, type); + AppendEnumBinding(body, sectionParam, propName, varName, type, configKey); } else { - AppendFixedEnumBinding(body, sectionParam, propName, varName, type); + AppendFixedEnumBinding(body, sectionParam, propName, varName, type, configKey); } } else if (type.IsStruct && TryGetStructUnderlyingType(type) is { } underlyingType) @@ -259,24 +280,24 @@ internal static void AppendBindingForProperty( // Use the constructor's parameter type to pick the correct binding. if (underlyingType.FrameworkType == typeof(string)) { - AppendEnumBinding(body, sectionParam, propName, varName, type); + AppendEnumBinding(body, sectionParam, propName, varName, type, configKey); } else if (underlyingType.FrameworkType == typeof(int) || underlyingType.FrameworkType == typeof(long)) { - AppendTryParseBinding(body, sectionParam, propName, varName, typeof(int)); + AppendTryParseBinding(body, sectionParam, propName, varName, typeof(int), configKey); } else if (underlyingType.FrameworkType == typeof(float) || underlyingType.FrameworkType == typeof(double)) { - AppendTryParseBinding(body, sectionParam, propName, varName, typeof(double)); + AppendTryParseBinding(body, sectionParam, propName, varName, typeof(double), configKey); } else { - AppendComplexObjectBinding(body, sectionParam, propName, varName, type); + AppendComplexObjectBinding(body, sectionParam, propName, varName, type, configKey); } } else { - AppendComplexObjectBinding(body, sectionParam, propName, varName, type); + AppendComplexObjectBinding(body, sectionParam, propName, varName, type, configKey); } return; } @@ -284,7 +305,7 @@ internal static void AppendBindingForProperty( // Handle collection types (string[]/List) if (type.IsList) { - AppendStringListBinding(body, sectionParam, propName, varName, type); + AppendStringListBinding(body, sectionParam, propName, varName, type, configKey); return; } @@ -292,39 +313,39 @@ internal static void AppendBindingForProperty( if (frameworkType == typeof(string)) { - AppendStringBinding(body, sectionParam, propName, varName); + AppendStringBinding(body, sectionParam, propName, varName, configKey); } else if (frameworkType == typeof(bool)) { - AppendTryParseBinding(body, sectionParam, propName, varName, typeof(bool)); + AppendTryParseBinding(body, sectionParam, propName, varName, typeof(bool), configKey); } else if (frameworkType == typeof(int)) { - AppendTryParseBinding(body, sectionParam, propName, varName, typeof(int)); + AppendTryParseBinding(body, sectionParam, propName, varName, typeof(int), configKey); } else if (frameworkType == typeof(long)) { - AppendTryParseBinding(body, sectionParam, propName, varName, typeof(long)); + AppendTryParseBinding(body, sectionParam, propName, varName, typeof(long), configKey); } else if (frameworkType == typeof(float)) { - AppendTryParseBinding(body, sectionParam, propName, varName, typeof(float)); + AppendTryParseBinding(body, sectionParam, propName, varName, typeof(float), configKey); } else if (frameworkType == typeof(double)) { - AppendTryParseBinding(body, sectionParam, propName, varName, typeof(double)); + AppendTryParseBinding(body, sectionParam, propName, varName, typeof(double), configKey); } else if (frameworkType == typeof(TimeSpan)) { - AppendTryParseBinding(body, sectionParam, propName, varName, typeof(TimeSpan)); + AppendTryParseBinding(body, sectionParam, propName, varName, typeof(TimeSpan), configKey); } else if (frameworkType == typeof(Uri)) { - AppendUriTryCreateBinding(body, sectionParam, propName, varName); + AppendUriTryCreateBinding(body, sectionParam, propName, varName, configKey); } else { - AppendComplexObjectBinding(body, sectionParam, propName, varName, type); + AppendComplexObjectBinding(body, sectionParam, propName, varName, type, configKey); } } @@ -335,9 +356,10 @@ internal static void AppendStringBinding( List body, ParameterProvider sectionParam, string propName, - string varName) + string varName, + string? configKey = null) { - body.Add(Declare(varName, new CSharpType(typeof(string), isNullable: true), new IndexerExpression(sectionParam, Literal(propName)), out var valVar)); + body.Add(Declare(varName, new CSharpType(typeof(string), isNullable: true), new IndexerExpression(sectionParam, Literal(configKey ?? propName)), out var valVar)); var ifStatement = new IfStatement(Not(StringSnippets.IsNullOrEmpty(valVar.As()))); ifStatement.Add(This.Property(propName).Assign(valVar).Terminate()); body.Add(ifStatement); @@ -351,13 +373,14 @@ internal static void AppendTryParseBinding( ParameterProvider sectionParam, string propName, string varName, - Type parseType) + Type parseType, + string? configKey = null) { var outDecl = new DeclarationExpression(parseType, varName, out var parsedVar, isOut: true); var ifStatement = new IfStatement(Static(parseType).Invoke("TryParse", new ValueExpression[] { - new IndexerExpression(sectionParam, Literal(propName)), + new IndexerExpression(sectionParam, Literal(configKey ?? propName)), outDecl })); ifStatement.Add(This.Property(propName).Assign(parsedVar).Terminate()); @@ -371,13 +394,14 @@ internal static void AppendUriTryCreateBinding( List body, ParameterProvider sectionParam, string propName, - string varName) + string varName, + string? configKey = null) { var outUriDecl = new DeclarationExpression(typeof(Uri), varName, out var uriVar, isOut: true); var ifStatement = new IfStatement(Static(typeof(Uri)).Invoke("TryCreate", new ValueExpression[] { - new IndexerExpression(sectionParam, Literal(propName)), + new IndexerExpression(sectionParam, Literal(configKey ?? propName)), new MemberExpression(typeof(UriKind), nameof(UriKind.Absolute)), outUriDecl })); @@ -394,7 +418,8 @@ internal static void AppendStringListBinding( ParameterProvider sectionParam, string propName, string varName, - CSharpType type) + CSharpType type, + string? configKey = null) { // Only handle List for now if (type.Arguments.Count == 0 || @@ -405,7 +430,7 @@ internal static void AppendStringListBinding( } // IConfigurationSection listSection = section.GetSection("PropName"); - body.Add(Declare((propName + "Section").ToVariableName(), IConfigurationSectionType, sectionParam.Invoke("GetSection", Literal(propName)), out var sectionVar)); + body.Add(Declare((propName + "Section").ToVariableName(), IConfigurationSectionType, sectionParam.Invoke("GetSection", Literal(configKey ?? propName)), out var sectionVar)); // if (listSection.Exists()) var ifExistsStatement = new IfStatement(sectionVar.Invoke("Exists")); @@ -437,10 +462,11 @@ internal static void AppendEnumBinding( ParameterProvider sectionParam, string propName, string varName, - CSharpType type) + CSharpType type, + string? configKey = null) { var decl = Declare(varName, new CSharpType(typeof(string)), out var declVar); - var ifStatement = new IfStatement(new IndexerExpression(sectionParam, Literal(propName)).Is(decl)); + var ifStatement = new IfStatement(new IndexerExpression(sectionParam, Literal(configKey ?? propName)).Is(decl)); ifStatement.Add(This.Property(propName).Assign(New.Instance(type, declVar)).Terminate()); body.Add(ifStatement); } @@ -453,13 +479,14 @@ internal static void AppendFixedEnumBinding( ParameterProvider sectionParam, string propName, string varName, - CSharpType type) + CSharpType type, + string? configKey = null) { var outDecl = new DeclarationExpression(type, varName, out var parsedVar, isOut: true); var ifStatement = new IfStatement(Static(typeof(Enum)).Invoke("TryParse", new ValueExpression[] { - new IndexerExpression(sectionParam, Literal(propName)), + new IndexerExpression(sectionParam, Literal(configKey ?? propName)), outDecl })); ifStatement.Add(This.Property(propName).Assign(parsedVar).Terminate()); @@ -475,10 +502,11 @@ internal static void AppendComplexObjectBinding( ParameterProvider sectionParam, string propName, string varName, - CSharpType type) + CSharpType type, + string? configKey = null) { // IConfigurationSection {name}Section = section.GetSection("PropName"); - body.Add(Declare((propName + "Section").ToVariableName(), IConfigurationSectionType, sectionParam.Invoke("GetSection", Literal(propName)), out var sectionVar)); + body.Add(Declare((propName + "Section").ToVariableName(), IConfigurationSectionType, sectionParam.Invoke("GetSection", Literal(configKey ?? propName)), out var sectionVar)); // if ({name}Section.Exists()) { PropName = new TypeName({name}Section); } var ifExistsStatement = new IfStatement(sectionVar.Invoke("Exists")); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientSettingsProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientSettingsProviderTests.cs index 4cf2db505d6..cf258937615 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientSettingsProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientSettingsProviderTests.cs @@ -1141,6 +1141,46 @@ await MockHelpers.LoadMockGeneratorAsync( "BindCore should bind the non-credential parameter 'TenantId'"); } + [Test] + public async Task TestBindCoreMethod_HonorsCustomPropertyRename() + { + // Custom code renames the generated 'Endpoint' settings property to 'ServiceUri' + // via [CodeGenMember("Endpoint")]. BindCore should assign to the renamed property + // while still reading from the original configuration key. + await MockHelpers.LoadMockGeneratorAsync( + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var inputParameters = new[] + { + InputFactory.EndpointParameter( + "endpoint", + InputPrimitiveType.Url, + scope: InputParameterScope.Client, + isEndpoint: true) + }; + var client = InputFactory.Client("TestClient", clientNamespace: "SampleNamespace", parameters: inputParameters); + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); + Assert.IsNotNull(clientProvider); + + var settingsProvider = clientProvider!.ClientSettings; + Assert.IsNotNull(settingsProvider); + Assert.IsNotNull(settingsProvider!.CustomCodeView, + "CustomCodeView should be available from the compilation"); + + // The generated 'Endpoint' property should be replaced by the custom 'ServiceUri' property. + Assert.IsNull(settingsProvider.Properties.FirstOrDefault(p => p.Name == "Endpoint"), + "Generated 'Endpoint' property should be replaced by the custom code rename"); + + var bindCoreMethod = settingsProvider.Methods.FirstOrDefault(m => m.Signature.Name == "BindCore"); + Assert.IsNotNull(bindCoreMethod); + + // Validate full generated output: BindCore should assign to the renamed 'ServiceUri' + // property while still reading from the original 'Endpoint' configuration key. + var writer = new TypeProviderWriter(settingsProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + [Test] public async Task TestGeneratedSettings_WithCustomizedBindCore() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_HonorsCustomPropertyRename.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_HonorsCustomPropertyRename.cs new file mode 100644 index 00000000000..93ac5b7e27b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_HonorsCustomPropertyRename.cs @@ -0,0 +1,30 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; + +namespace SampleNamespace +{ + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0002")] + public partial class TestClientSettings : global::System.ClientModel.Primitives.ClientSettings + { + public global::SampleNamespace.TestClientOptions Options { get; set; } + + protected override void BindCore(global::Microsoft.Extensions.Configuration.IConfigurationSection section) + { + if (global::System.Uri.TryCreate(section["Endpoint"], global::System.UriKind.Absolute, out global::System.Uri endpoint)) + { + this.ServiceUri = endpoint; + } + global::Microsoft.Extensions.Configuration.IConfigurationSection optionsSection = section.GetSection("Options"); + if (optionsSection.Exists()) + { + this.Options = new global::SampleNamespace.TestClientOptions(optionsSection); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_HonorsCustomPropertyRename/TestClientSettings.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_HonorsCustomPropertyRename/TestClientSettings.cs new file mode 100644 index 00000000000..313c84ba6d1 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientSettingsProviderTests/TestBindCoreMethod_HonorsCustomPropertyRename/TestClientSettings.cs @@ -0,0 +1,13 @@ +#nullable disable + +using System; +using Microsoft.TypeSpec.Generator.Customizations; + +namespace SampleNamespace +{ + public partial class TestClientSettings + { + [CodeGenMember("Endpoint")] + public Uri ServiceUri { get; set; } + } +}