Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
@@ -0,0 +1,7 @@
---
Comment thread
jorgerangel-msft marked this conversation as resolved.
Outdated
changeKind: fix
packages:
- "@typespec/http-client-csharp"
---

Honor custom code property replacements (e.g. `[CodeGenMember("Url")]`) in the generated `ClientSettings.BindCore` method so the binding assigns to the renamed property while still reading from the original configuration key.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -229,6 +229,28 @@ protected override MethodProvider[] BuildMethods()
return [bindCoreMethod];
}

/// <summary>
/// Resolves the effective property name to assign to during binding, honoring custom code
/// replacements (e.g., a property renamed via <c>[CodeGenMember("OriginalName")]</c>).
/// The configuration key used to read the value remains the original generated name.
/// </summary>
private string GetSettingPropertyName(string generatedName)
{
var customProperties = CustomCodeView?.Properties;
Comment thread
jorgerangel-msft marked this conversation as resolved.
Outdated
if (customProperties != null)
{
foreach (var customProperty in customProperties)
{
if (customProperty.OriginalName == generatedName)
{
return customProperty.Name;
}
}
}

return generatedName;
}

/// <summary>
/// Dispatches to the appropriate binding method based on the property type.
/// </summary>
Expand All @@ -237,20 +259,23 @@ internal static void AppendBindingForProperty(
ParameterProvider sectionParam,
string propName,
string varName,
CSharpType type)
CSharpType type,
string? configKey = null)
Comment thread
jorgerangel-msft marked this conversation as resolved.
{
configKey ??= propName;

// Handle non-framework types (enums, complex objects)
if (!type.IsFrameworkType)
{
if (type.IsEnum)
{
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)
Expand All @@ -259,72 +284,72 @@ 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;
}

// Handle collection types (string[]/List<string>)
if (type.IsList)
{
AppendStringListBinding(body, sectionParam, propName, varName, type);
AppendStringListBinding(body, sectionParam, propName, varName, type, configKey);
return;
}

var frameworkType = type.FrameworkType;

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);
}
}

Expand All @@ -335,9 +360,10 @@ internal static void AppendStringBinding(
List<MethodBodyStatement> 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<string>())));
ifStatement.Add(This.Property(propName).Assign(valVar).Terminate());
body.Add(ifStatement);
Expand All @@ -351,13 +377,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());
Expand All @@ -371,13 +398,14 @@ internal static void AppendUriTryCreateBinding(
List<MethodBodyStatement> 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
}));
Expand All @@ -394,7 +422,8 @@ internal static void AppendStringListBinding(
ParameterProvider sectionParam,
string propName,
string varName,
CSharpType type)
CSharpType type,
string? configKey = null)
{
// Only handle List<string> for now
if (type.Arguments.Count == 0 ||
Expand All @@ -405,7 +434,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"));
Expand Down Expand Up @@ -437,10 +466,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);
}
Expand All @@ -453,13 +483,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());
Expand All @@ -475,10 +506,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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1140,5 +1140,49 @@ await MockHelpers.LoadMockGeneratorAsync(
Assert.IsTrue(bodyString.Contains("TenantId"),
"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;
Comment thread
jorgerangel-msft marked this conversation as resolved.
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);

var bodyString = bindCoreMethod!.BodyStatements!.ToDisplayString();
// Assignment target should be the renamed property.
Assert.IsTrue(bodyString.Contains("ServiceUri = "),
"BindCore should assign to the renamed 'ServiceUri' property");
Assert.IsFalse(bodyString.Contains("this.Endpoint = ") || bodyString.Contains("Endpoint = endpoint"),
"BindCore should not assign to the original 'Endpoint' property after the custom rename");
// Configuration key should remain the original generated name.
Assert.IsTrue(bodyString.Contains("section[\"Endpoint\"]"),
"BindCore should still read from the original 'Endpoint' configuration key");
}
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Loading