Skip to content

Commit cf47d9f

Browse files
authored
Fix binding types with optional string parameters (#93563)
* Add a baseline test for constructor parameters * Fix binding types with optional string parameters Ensure the source generator emits the declaration with default value for all cases when we emit the bind logic. * Add a test case that uses a Primary Constructor with default values * Split baseline data for added test. * Fix .NETFramework test baseline * Update baselines after global namespace change
1 parent 73cb06e commit cf47d9f

File tree

6 files changed

+588
-17
lines changed

6 files changed

+588
-17
lines changed

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,6 @@ void EmitBindImplForMember(MemberSpec member)
379379
TypeSpec memberType = _typeIndex.GetTypeSpec(member.TypeRef);
380380
string parsedMemberDeclarationLhs = $"{memberType.TypeRef.FullyQualifiedName} {member.Name}";
381381
string configKeyName = member.ConfigurationKeyName;
382-
string parsedMemberAssignmentLhsExpr;
383382

384383
switch (memberType)
385384
{
@@ -392,31 +391,22 @@ void EmitBindImplForMember(MemberSpec member)
392391
_writer.WriteLine();
393392
return;
394393
}
395-
396-
parsedMemberAssignmentLhsExpr = parsedMemberDeclarationLhs;
397394
}
398395
break;
399396
case ConfigurationSectionSpec:
400397
{
401398
_writer.WriteLine($"{parsedMemberDeclarationLhs} = {GetSectionFromConfigurationExpression(configKeyName)};");
402399
return;
403400
}
404-
default:
405-
{
406-
string bangExpr = memberType.IsValueType ? string.Empty : "!";
407-
string parsedMemberIdentifierDeclaration = $"{parsedMemberDeclarationLhs} = {member.DefaultValueExpr}{bangExpr};";
408-
409-
_writer.WriteLine(parsedMemberIdentifierDeclaration);
410-
_emitBlankLineBeforeNextStatement = false;
411-
412-
parsedMemberAssignmentLhsExpr = member.Name;
413-
}
414-
break;
415401
}
416402

403+
string bangExpr = memberType.IsValueType ? string.Empty : "!";
404+
_writer.WriteLine($"{parsedMemberDeclarationLhs} = {member.DefaultValueExpr}{bangExpr};");
405+
_emitBlankLineBeforeNextStatement = false;
406+
417407
bool canBindToMember = this.EmitBindImplForMember(
418408
member,
419-
parsedMemberAssignmentLhsExpr,
409+
member.Name,
420410
sectionPathExpr: GetSectionPathFromConfigurationExpression(configKeyName),
421411
canSet: true,
422412
InitializationKind.None);

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public class ClassWhereParametersHaveDefaultValue
129129
public StringComparison? NSC { get; }
130130
public char? NC { get; }
131131

132-
public ClassWhereParametersHaveDefaultValue(string? name, string address,
132+
public ClassWhereParametersHaveDefaultValue(string? name = "John Doe", string address = "1 Microsoft Way",
133133
int age = 42, float f = 42.0f, double d = 3.14159, decimal m = 3.1415926535897932384626433M, StringComparison sc = StringComparison.Ordinal, char c = 'q',
134134
int? nage = 42, float? nf = 42.0f, double? nd = 3.14159, decimal? nm = 3.1415926535897932384626433M, StringComparison? nsc = StringComparison.Ordinal, char? nc = 'q')
135135
{
@@ -150,13 +150,19 @@ public ClassWhereParametersHaveDefaultValue(string? name, string address,
150150
}
151151
}
152152

153-
154153
public class ClassWithPrimaryCtor(string color, int length)
155154
{
156155
public string Color { get; } = color;
157156
public int Length { get; } = length;
158157
}
159158

159+
public class ClassWithPrimaryCtorDefaultValues(string color = "blue", int length = 15, decimal height = 5.946238490567943927384M, EditorBrowsableState eb = EditorBrowsableState.Never)
160+
{
161+
public string Color { get; } = color;
162+
public int Length { get; } = length;
163+
public decimal Height { get; } = height;
164+
public EditorBrowsableState EB { get;} = eb;
165+
}
160166
public record RecordTypeOptions(string Color, int Length);
161167

162168
public record Line(string Color, int Length, int Thickness);

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,24 @@ public void CanBindClassWithPrimaryCtor()
13981398
Assert.Equal("Green", options.Color);
13991399
}
14001400

1401+
[Fact]
1402+
public void CanBindClassWithPrimaryCtorWithDefaultValues()
1403+
{
1404+
var dic = new Dictionary<string, string>
1405+
{
1406+
{"Length", "-1"}
1407+
};
1408+
var configurationBuilder = new ConfigurationBuilder();
1409+
configurationBuilder.AddInMemoryCollection(dic);
1410+
var config = configurationBuilder.Build();
1411+
1412+
var options = config.Get<ClassWithPrimaryCtorDefaultValues>();
1413+
Assert.Equal(-1, options.Length);
1414+
Assert.Equal("blue", options.Color);
1415+
Assert.Equal(5.946238490567943927384M, options.Height);
1416+
Assert.Equal(EditorBrowsableState.Never, options.EB);
1417+
}
1418+
14011419
[Fact]
14021420
public void CanBindRecordStructOptions()
14031421
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// <auto-generated/>
2+
3+
#nullable enable annotations
4+
#nullable disable warnings
5+
6+
// Suppress warnings about [Obsolete] member usage in generated code.
7+
#pragma warning disable CS0612, CS0618
8+
9+
namespace System.Runtime.CompilerServices
10+
{
11+
using System;
12+
using System.CodeDom.Compiler;
13+
14+
[GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "42.42.42.42")]
15+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
16+
file sealed class InterceptsLocationAttribute : Attribute
17+
{
18+
public InterceptsLocationAttribute(string filePath, int line, int column)
19+
{
20+
}
21+
}
22+
}
23+
24+
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
25+
{
26+
using Microsoft.Extensions.Configuration;
27+
using System;
28+
using System.CodeDom.Compiler;
29+
using System.Collections.Generic;
30+
using System.Globalization;
31+
using System.Runtime.CompilerServices;
32+
33+
[GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "42.42.42.42")]
34+
file static class BindingExtensions
35+
{
36+
#region IConfiguration extensions.
37+
/// <summary>Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively.</summary>
38+
[InterceptsLocation(@"src-0.cs", 13, 16)]
39+
public static void Bind_ProgramClassWhereParametersHaveDefaultValue(this IConfiguration configuration, object? instance)
40+
{
41+
if (configuration is null)
42+
{
43+
throw new ArgumentNullException(nameof(configuration));
44+
}
45+
46+
if (instance is null)
47+
{
48+
return;
49+
}
50+
51+
var typedObj = (global::Program.ClassWhereParametersHaveDefaultValue)instance;
52+
BindCore(configuration, ref typedObj, defaultValueIfNotFound: false, binderOptions: null);
53+
}
54+
#endregion IConfiguration extensions.
55+
56+
#region Core binding extensions.
57+
private readonly static Lazy<HashSet<string>> s_configKeys_ProgramClassWhereParametersHaveDefaultValue = new(() => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Name", "Address", "Age", "F", "D", "M", "SC", "C", "NAge", "NF", "ND", "NM", "NSC", "NC" });
58+
59+
public static void BindCore(IConfiguration configuration, ref global::Program.ClassWhereParametersHaveDefaultValue instance, bool defaultValueIfNotFound, BinderOptions? binderOptions)
60+
{
61+
ValidateConfigurationKeys(typeof(global::Program.ClassWhereParametersHaveDefaultValue), s_configKeys_ProgramClassWhereParametersHaveDefaultValue, configuration, binderOptions);
62+
}
63+
64+
public static global::Program.ClassWhereParametersHaveDefaultValue InitializeProgramClassWhereParametersHaveDefaultValue(IConfiguration configuration, BinderOptions? binderOptions)
65+
{
66+
string name = "John Doe"!;
67+
if (configuration["Name"] is string value0)
68+
{
69+
name = value0;
70+
}
71+
72+
string address = "1 Microsoft Way"!;
73+
if (configuration["Address"] is string value1)
74+
{
75+
address = value1;
76+
}
77+
78+
int age = (int)(42);
79+
if (configuration["Age"] is string value2)
80+
{
81+
age = ParseInt(value2, () => configuration.GetSection("Age").Path);
82+
}
83+
84+
float f = 42F;
85+
if (configuration["F"] is string value3)
86+
{
87+
f = ParseFloat(value3, () => configuration.GetSection("F").Path);
88+
}
89+
90+
double d = 3.1415899999999999D;
91+
if (configuration["D"] is string value4)
92+
{
93+
d = ParseDouble(value4, () => configuration.GetSection("D").Path);
94+
}
95+
96+
decimal m = 3.1415926535897932384626433M;
97+
if (configuration["M"] is string value5)
98+
{
99+
m = ParseDecimal(value5, () => configuration.GetSection("M").Path);
100+
}
101+
102+
global::System.StringComparison sc = (global::System.StringComparison)(4);
103+
if (configuration["SC"] is string value6)
104+
{
105+
sc = ParseEnum<global::System.StringComparison>(value6, () => configuration.GetSection("SC").Path);
106+
}
107+
108+
char c = 'q';
109+
if (configuration["C"] is string value7)
110+
{
111+
c = ParseChar(value7, () => configuration.GetSection("C").Path);
112+
}
113+
114+
int? nage = (int?)(42);
115+
if (configuration["NAge"] is string value8)
116+
{
117+
nage = ParseInt(value8, () => configuration.GetSection("NAge").Path);
118+
}
119+
120+
float? nf = 42F;
121+
if (configuration["NF"] is string value9)
122+
{
123+
nf = ParseFloat(value9, () => configuration.GetSection("NF").Path);
124+
}
125+
126+
double? nd = 3.1415899999999999D;
127+
if (configuration["ND"] is string value10)
128+
{
129+
nd = ParseDouble(value10, () => configuration.GetSection("ND").Path);
130+
}
131+
132+
decimal? nm = 3.1415926535897932384626433M;
133+
if (configuration["NM"] is string value11)
134+
{
135+
nm = ParseDecimal(value11, () => configuration.GetSection("NM").Path);
136+
}
137+
138+
global::System.StringComparison? nsc = (global::System.StringComparison?)(4);
139+
if (configuration["NSC"] is string value12)
140+
{
141+
nsc = ParseEnum<global::System.StringComparison>(value12, () => configuration.GetSection("NSC").Path);
142+
}
143+
144+
char? nc = 'q';
145+
if (configuration["NC"] is string value13)
146+
{
147+
nc = ParseChar(value13, () => configuration.GetSection("NC").Path);
148+
}
149+
150+
return new global::Program.ClassWhereParametersHaveDefaultValue(name, address, age, f, d, m, sc, c, nage, nf, nd, nm, nsc, nc);
151+
}
152+
153+
154+
/// <summary>If required by the binder options, validates that there are no unknown keys in the input configuration object.</summary>
155+
public static void ValidateConfigurationKeys(Type type, Lazy<HashSet<string>> keys, IConfiguration configuration, BinderOptions? binderOptions)
156+
{
157+
if (binderOptions?.ErrorOnUnknownConfiguration is true)
158+
{
159+
List<string>? temp = null;
160+
161+
foreach (IConfigurationSection section in configuration.GetChildren())
162+
{
163+
if (!keys.Value.Contains(section.Key))
164+
{
165+
(temp ??= new List<string>()).Add($"'{section.Key}'");
166+
}
167+
}
168+
169+
if (temp is not null)
170+
{
171+
throw new InvalidOperationException($"'ErrorOnUnknownConfiguration' was set on the provided BinderOptions, but the following properties were not found on the instance of {type}: {string.Join(", ", temp)}");
172+
}
173+
}
174+
}
175+
176+
public static T ParseEnum<T>(string value, Func<string?> getPath) where T : struct
177+
{
178+
try
179+
{
180+
return (T)Enum.Parse(typeof(T), value, ignoreCase: true);
181+
}
182+
catch (Exception exception)
183+
{
184+
throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(T)}'.", exception);
185+
}
186+
}
187+
188+
public static int ParseInt(string value, Func<string?> getPath)
189+
{
190+
try
191+
{
192+
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
193+
}
194+
catch (Exception exception)
195+
{
196+
throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(int)}'.", exception);
197+
}
198+
}
199+
200+
public static float ParseFloat(string value, Func<string?> getPath)
201+
{
202+
try
203+
{
204+
return float.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
205+
}
206+
catch (Exception exception)
207+
{
208+
throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(float)}'.", exception);
209+
}
210+
}
211+
212+
public static double ParseDouble(string value, Func<string?> getPath)
213+
{
214+
try
215+
{
216+
return double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
217+
}
218+
catch (Exception exception)
219+
{
220+
throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(double)}'.", exception);
221+
}
222+
}
223+
224+
public static decimal ParseDecimal(string value, Func<string?> getPath)
225+
{
226+
try
227+
{
228+
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
229+
}
230+
catch (Exception exception)
231+
{
232+
throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(decimal)}'.", exception);
233+
}
234+
}
235+
236+
public static char ParseChar(string value, Func<string?> getPath)
237+
{
238+
try
239+
{
240+
return char.Parse(value);
241+
}
242+
catch (Exception exception)
243+
{
244+
throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(char)}'.", exception);
245+
}
246+
}
247+
#endregion Core binding extensions.
248+
}
249+
}

0 commit comments

Comments
 (0)