diff --git a/src/libraries/Common/src/SourceGenerators/CSharpSyntaxUtilities.cs b/src/libraries/Common/src/SourceGenerators/CSharpSyntaxUtilities.cs
new file mode 100644
index 00000000000000..8cd2f1850b718a
--- /dev/null
+++ b/src/libraries/Common/src/SourceGenerators/CSharpSyntaxUtilities.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Globalization;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace SourceGenerators;
+
+internal static class CSharpSyntaxUtilities
+{
+ // Standard format for double and single on non-inbox frameworks to ensure value is round-trippable.
+ public const string DoubleFormatString = "G17";
+ public const string SingleFormatString = "G9";
+
+ // Format a literal in C# format -- works around https://github.com/dotnet/roslyn/issues/58705
+ public static string FormatLiteral(object? value, TypeRef type)
+ {
+ if (value == null)
+ {
+ return $"default({type.FullyQualifiedName})";
+ }
+
+ switch (value)
+ {
+ case string @string:
+ return SymbolDisplay.FormatLiteral(@string, quote: true); ;
+ case char @char:
+ return SymbolDisplay.FormatLiteral(@char, quote: true);
+ case double.NegativeInfinity:
+ return "double.NegativeInfinity";
+ case double.PositiveInfinity:
+ return "double.PositiveInfinity";
+ case double.NaN:
+ return "double.NaN";
+ case double @double:
+ return $"{@double.ToString(DoubleFormatString, CultureInfo.InvariantCulture)}D";
+ case float.NegativeInfinity:
+ return "float.NegativeInfinity";
+ case float.PositiveInfinity:
+ return "float.PositiveInfinity";
+ case float.NaN:
+ return "float.NaN";
+ case float @float:
+ return $"{@float.ToString(SingleFormatString, CultureInfo.InvariantCulture)}F";
+ case decimal @decimal:
+ // we do not need to specify a format string for decimal as it's default is round-trippable on all frameworks.
+ return $"{@decimal.ToString(CultureInfo.InvariantCulture)}M";
+ case bool @bool:
+ return @bool ? "true" : "false";
+ default:
+ // Assume this is a number.
+ return FormatNumber();
+ }
+
+ string FormatNumber() => $"({type.FullyQualifiedName})({Convert.ToString(value, CultureInfo.InvariantCulture)})";
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs
index 17b1da25f808d7..849670bc04376c 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs
@@ -582,9 +582,8 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo)
AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute));
string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName;
- PropertySpec spec = new(property)
+ PropertySpec spec = new(property, propertyTypeRef)
{
- TypeRef = propertyTypeRef,
ConfigurationKeyName = configKeyName
};
@@ -616,9 +615,8 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo)
}
else
{
- ParameterSpec paramSpec = new ParameterSpec(parameter)
+ ParameterSpec paramSpec = new ParameterSpec(parameter, propertySpec.TypeRef)
{
- TypeRef = propertySpec.TypeRef,
ConfigurationKeyName = propertySpec.ConfigurationKeyName,
};
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs
index 50cfb381982b03..4f72740d91335c 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs
@@ -379,7 +379,6 @@ void EmitBindImplForMember(MemberSpec member)
TypeSpec memberType = _typeIndex.GetTypeSpec(member.TypeRef);
string parsedMemberDeclarationLhs = $"{memberType.TypeRef.FullyQualifiedName} {member.Name}";
string configKeyName = member.ConfigurationKeyName;
- string parsedMemberAssignmentLhsExpr;
switch (memberType)
{
@@ -392,8 +391,6 @@ void EmitBindImplForMember(MemberSpec member)
_writer.WriteLine();
return;
}
-
- parsedMemberAssignmentLhsExpr = parsedMemberDeclarationLhs;
}
break;
case ConfigurationSectionSpec:
@@ -401,22 +398,15 @@ void EmitBindImplForMember(MemberSpec member)
_writer.WriteLine($"{parsedMemberDeclarationLhs} = {GetSectionFromConfigurationExpression(configKeyName)};");
return;
}
- default:
- {
- string bangExpr = memberType.IsValueType ? string.Empty : "!";
- string parsedMemberIdentifierDeclaration = $"{parsedMemberDeclarationLhs} = {member.DefaultValueExpr}{bangExpr};";
-
- _writer.WriteLine(parsedMemberIdentifierDeclaration);
- _emitBlankLineBeforeNextStatement = false;
-
- parsedMemberAssignmentLhsExpr = member.Name;
- }
- break;
}
+ string bangExpr = memberType.IsValueType ? string.Empty : "!";
+ _writer.WriteLine($"{parsedMemberDeclarationLhs} = {member.DefaultValueExpr}{bangExpr};");
+ _emitBlankLineBeforeNextStatement = false;
+
bool canBindToMember = this.EmitBindImplForMember(
member,
- parsedMemberAssignmentLhsExpr,
+ member.Name,
sectionPathExpr: GetSectionPathFromConfigurationExpression(configKeyName),
canSet: true,
InitializationKind.None);
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj
index 764682b43daa86..a560bbfe928aa7 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj
@@ -30,6 +30,7 @@
+
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs
index dc5b03087ac87a..cbc205ec2976ff 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs
@@ -9,17 +9,18 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
public abstract record MemberSpec
{
- public MemberSpec(ISymbol member)
+ public MemberSpec(ISymbol member, TypeRef typeRef)
{
Debug.Assert(member is IPropertySymbol or IParameterSymbol);
Name = member.Name;
DefaultValueExpr = "default";
+ TypeRef = typeRef;
}
public string Name { get; }
public string DefaultValueExpr { get; protected set; }
- public required TypeRef TypeRef { get; init; }
+ public TypeRef TypeRef { get; }
public required string ConfigurationKeyName { get; init; }
public abstract bool CanGet { get; }
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs
index 62c781e1f1631f..53ca14ae7eedec 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs
@@ -1,24 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Globalization;
+using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
+using SourceGenerators;
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
public sealed record ParameterSpec : MemberSpec
{
- public ParameterSpec(IParameterSymbol parameter) : base(parameter)
+ public ParameterSpec(IParameterSymbol parameter, TypeRef typeRef) : base(parameter, typeRef)
{
RefKind = parameter.RefKind;
if (parameter.HasExplicitDefaultValue)
{
- string formatted = SymbolDisplay.FormatPrimitive(parameter.ExplicitDefaultValue!, quoteStrings: true, useHexadecimalNumbers: false);
- if (formatted is not "null")
- {
- DefaultValueExpr = formatted;
- }
+ DefaultValueExpr = CSharpSyntaxUtilities.FormatLiteral(parameter.ExplicitDefaultValue, TypeRef);
}
else
{
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs
index 443e39d32e4933..66257d06cab891 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs
@@ -2,12 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.CodeAnalysis;
+using SourceGenerators;
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
public sealed record PropertySpec : MemberSpec
{
- public PropertySpec(IPropertySymbol property) : base(property)
+ public PropertySpec(IPropertySymbol property, TypeRef typeRef) : base(property, typeRef)
{
IMethodSymbol? setMethod = property.SetMethod;
bool setterIsPublic = setMethod?.DeclaredAccessibility is Accessibility.Public;
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs
index 7d10f66c822fc0..9aad9566463aff 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs
@@ -112,17 +112,41 @@ public ClassWhereParametersMatchPropertiesAndFields(string name, string address,
public record RecordWhereParametersHaveDefaultValue(string Name, string Address, int Age = 42);
- public record ClassWhereParametersHaveDefaultValue
+ public class ClassWhereParametersHaveDefaultValue
{
public string? Name { get; }
public string Address { get; }
public int Age { get; }
-
- public ClassWhereParametersHaveDefaultValue(string? name, string address, int age = 42)
+ public float F { get; }
+ public double D { get; }
+ public decimal M { get; }
+ public StringComparison SC { get; }
+ public char C { get; }
+ public int? NAge { get; }
+ public float? NF { get; }
+ public double? ND { get; }
+ public decimal? NM { get; }
+ public StringComparison? NSC { get; }
+ public char? NC { get; }
+
+ public ClassWhereParametersHaveDefaultValue(string? name = "John Doe", string address = "1 Microsoft Way",
+ int age = 42, float f = 42.0f, double d = 3.14159, decimal m = 3.1415926535897932384626433M, StringComparison sc = StringComparison.Ordinal, char c = 'q',
+ int? nage = 42, float? nf = 42.0f, double? nd = 3.14159, decimal? nm = 3.1415926535897932384626433M, StringComparison? nsc = StringComparison.Ordinal, char? nc = 'q')
{
Name = name;
Address = address;
Age = age;
+ F = f;
+ D = d;
+ M = m;
+ SC = sc;
+ C = c;
+ NAge = nage;
+ NF = nf;
+ ND = nd;
+ NM = nm;
+ NSC = nsc;
+ NC = nc;
}
}
@@ -132,6 +156,13 @@ public class ClassWithPrimaryCtor(string color, int length)
public int Length { get; } = length;
}
+ public class ClassWithPrimaryCtorDefaultValues(string color = "blue", int length = 15, decimal height = 5.946238490567943927384M, EditorBrowsableState eb = EditorBrowsableState.Never)
+ {
+ public string Color { get; } = color;
+ public int Length { get; } = length;
+ public decimal Height { get; } = height;
+ public EditorBrowsableState EB { get;} = eb;
+ }
public record RecordTypeOptions(string Color, int Length);
public record Line(string Color, int Length, int Thickness);
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs
index 296cb790c22ba5..2103749861348d 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs
@@ -1064,6 +1064,17 @@ public void BindsToClassConstructorParametersWithDefaultValues()
Assert.Equal("John", testOptions.ClassWhereParametersHaveDefaultValueProperty.Name);
Assert.Equal("123, Abc St.", testOptions.ClassWhereParametersHaveDefaultValueProperty.Address);
Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.Age);
+ Assert.Equal(42.0f, testOptions.ClassWhereParametersHaveDefaultValueProperty.F);
+ Assert.Equal(3.14159, testOptions.ClassWhereParametersHaveDefaultValueProperty.D);
+ Assert.Equal(3.1415926535897932384626433M, testOptions.ClassWhereParametersHaveDefaultValueProperty.M);
+ Assert.Equal(StringComparison.Ordinal, testOptions.ClassWhereParametersHaveDefaultValueProperty.SC);
+ Assert.Equal('q', testOptions.ClassWhereParametersHaveDefaultValueProperty.C);
+ Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.NAge);
+ Assert.Equal(42.0f, testOptions.ClassWhereParametersHaveDefaultValueProperty.NF);
+ Assert.Equal(3.14159, testOptions.ClassWhereParametersHaveDefaultValueProperty.ND);
+ Assert.Equal(3.1415926535897932384626433M, testOptions.ClassWhereParametersHaveDefaultValueProperty.NM);
+ Assert.Equal(StringComparison.Ordinal, testOptions.ClassWhereParametersHaveDefaultValueProperty.NSC);
+ Assert.Equal('q', testOptions.ClassWhereParametersHaveDefaultValueProperty.NC);
}
[Fact]
@@ -1404,6 +1415,24 @@ public void CanBindClassWithPrimaryCtor()
Assert.Equal("Green", options.Color);
}
+ [Fact]
+ public void CanBindClassWithPrimaryCtorWithDefaultValues()
+ {
+ var dic = new Dictionary
+ {
+ {"Length", "-1"}
+ };
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddInMemoryCollection(dic);
+ var config = configurationBuilder.Build();
+
+ var options = config.Get();
+ Assert.Equal(-1, options.Length);
+ Assert.Equal("blue", options.Color);
+ Assert.Equal(5.946238490567943927384M, options.Height);
+ Assert.Equal(EditorBrowsableState.Never, options.EB);
+ }
+
[Fact]
public void CanBindRecordStructOptions()
{
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/DefaultConstructorParameters.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/DefaultConstructorParameters.generated.txt
new file mode 100644
index 00000000000000..98dcd4e471e248
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/DefaultConstructorParameters.generated.txt
@@ -0,0 +1,253 @@
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+namespace System.Runtime.CompilerServices
+{
+ using System;
+ using System.CodeDom.Compiler;
+
+ [GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "42.42.42.42")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : Attribute
+ {
+ public InterceptsLocationAttribute(string filePath, int line, int column)
+ {
+ }
+ }
+}
+
+namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
+{
+ using Microsoft.Extensions.Configuration;
+ using System;
+ using System.CodeDom.Compiler;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.Runtime.CompilerServices;
+
+ [GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "42.42.42.42")]
+ file static class BindingExtensions
+ {
+ #region IConfiguration extensions.
+ /// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively.
+ [InterceptsLocation(@"src-0.cs", 13, 16)]
+ public static void Bind_ProgramClassWhereParametersHaveDefaultValue(this IConfiguration configuration, object? instance)
+ {
+ if (configuration is null)
+ {
+ throw new ArgumentNullException(nameof(configuration));
+ }
+
+ if (instance is null)
+ {
+ return;
+ }
+
+ var typedObj = (global::Program.ClassWhereParametersHaveDefaultValue)instance;
+ BindCore(configuration, ref typedObj, defaultValueIfNotFound: false, binderOptions: null);
+ }
+ #endregion IConfiguration extensions.
+
+ #region Core binding extensions.
+ private readonly static Lazy> s_configKeys_ProgramClassWhereParametersHaveDefaultValue = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "Name", "Address", "Age", "F", "D", "M", "SC", "C", "NAge", "NF", "ND", "NM", "NSC", "NC" });
+
+ public static void BindCore(IConfiguration configuration, ref global::Program.ClassWhereParametersHaveDefaultValue instance, bool defaultValueIfNotFound, BinderOptions? binderOptions)
+ {
+ ValidateConfigurationKeys(typeof(global::Program.ClassWhereParametersHaveDefaultValue), s_configKeys_ProgramClassWhereParametersHaveDefaultValue, configuration, binderOptions);
+ }
+
+ public static global::Program.ClassWhereParametersHaveDefaultValue InitializeProgramClassWhereParametersHaveDefaultValue(IConfiguration configuration, BinderOptions? binderOptions)
+ {
+ string name = "John Doe"!;
+ if (configuration["Name"] is string value0)
+ {
+ name = value0;
+ }
+
+ string address = "1 Microsoft Way"!;
+ if (configuration["Address"] is string value1)
+ {
+ address = value1;
+ }
+
+ int age = (int)(42);
+ if (configuration["Age"] is string value2)
+ {
+ age = ParseInt(value2, () => configuration.GetSection("Age").Path);
+ }
+
+ float f = 42F;
+ if (configuration["F"] is string value3)
+ {
+ f = ParseFloat(value3, () => configuration.GetSection("F").Path);
+ }
+
+ double d = 3.1415899999999999D;
+ if (configuration["D"] is string value4)
+ {
+ d = ParseDouble(value4, () => configuration.GetSection("D").Path);
+ }
+
+ decimal m = 3.1415926535897932384626433M;
+ if (configuration["M"] is string value5)
+ {
+ m = ParseDecimal(value5, () => configuration.GetSection("M").Path);
+ }
+
+ global::System.StringComparison sc = (global::System.StringComparison)(4);
+ if (configuration["SC"] is string value6)
+ {
+ sc = ParseEnum(value6, () => configuration.GetSection("SC").Path);
+ }
+
+ char c = 'q';
+ if (configuration["C"] is string value7)
+ {
+ c = ParseChar(value7, () => configuration.GetSection("C").Path);
+ }
+
+ int? nage = (int?)(42);
+ if (configuration["NAge"] is string value8)
+ {
+ nage = ParseInt(value8, () => configuration.GetSection("NAge").Path);
+ }
+
+ float? nf = 42F;
+ if (configuration["NF"] is string value9)
+ {
+ nf = ParseFloat(value9, () => configuration.GetSection("NF").Path);
+ }
+
+ double? nd = 3.1415899999999999D;
+ if (configuration["ND"] is string value10)
+ {
+ nd = ParseDouble(value10, () => configuration.GetSection("ND").Path);
+ }
+
+ decimal? nm = 3.1415926535897932384626433M;
+ if (configuration["NM"] is string value11)
+ {
+ nm = ParseDecimal(value11, () => configuration.GetSection("NM").Path);
+ }
+
+ global::System.StringComparison? nsc = (global::System.StringComparison?)(4);
+ if (configuration["NSC"] is string value12)
+ {
+ nsc = ParseEnum(value12, () => configuration.GetSection("NSC").Path);
+ }
+
+ char? nc = 'q';
+ if (configuration["NC"] is string value13)
+ {
+ nc = ParseChar(value13, () => configuration.GetSection("NC").Path);
+ }
+
+ return new global::Program.ClassWhereParametersHaveDefaultValue(name, address, age, f, d, m, sc, c, nage, nf, nd, nm, nsc, nc);
+ }
+
+
+ /// If required by the binder options, validates that there are no unknown keys in the input configuration object.
+ public static void ValidateConfigurationKeys(Type type, Lazy> keys, IConfiguration configuration, BinderOptions? binderOptions)
+ {
+ if (binderOptions?.ErrorOnUnknownConfiguration is true)
+ {
+ List? temp = null;
+
+ foreach (IConfigurationSection section in configuration.GetChildren())
+ {
+ if (!keys.Value.Contains(section.Key))
+ {
+ (temp ??= new List()).Add($"'{section.Key}'");
+ }
+ }
+
+ if (temp is not null)
+ {
+ 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)}");
+ }
+ }
+ }
+
+ public static int ParseInt(string value, Func getPath)
+ {
+ try
+ {
+ return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ }
+ catch (Exception exception)
+ {
+ throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(int)}'.", exception);
+ }
+ }
+
+ public static float ParseFloat(string value, Func getPath)
+ {
+ try
+ {
+ return float.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
+ }
+ catch (Exception exception)
+ {
+ throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(float)}'.", exception);
+ }
+ }
+
+ public static double ParseDouble(string value, Func getPath)
+ {
+ try
+ {
+ return double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
+ }
+ catch (Exception exception)
+ {
+ throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(double)}'.", exception);
+ }
+ }
+
+ public static decimal ParseDecimal(string value, Func getPath)
+ {
+ try
+ {
+ return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
+ }
+ catch (Exception exception)
+ {
+ throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(decimal)}'.", exception);
+ }
+ }
+
+ public static T ParseEnum(string value, Func getPath) where T : struct
+ {
+ try
+ {
+ #if NETFRAMEWORK || NETSTANDARD2_0
+ return (T)Enum.Parse(typeof(T), value, ignoreCase: true);
+ #else
+ return Enum.Parse(value, ignoreCase: true);
+ #endif
+ }
+ catch (Exception exception)
+ {
+ throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(T)}'.", exception);
+ }
+ }
+
+ public static char ParseChar(string value, Func getPath)
+ {
+ try
+ {
+ return char.Parse(value);
+ }
+ catch (Exception exception)
+ {
+ throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(char)}'.", exception);
+ }
+ }
+ #endregion Core binding extensions.
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs
index e05a7737137128..8dd225ba4227b3 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs
@@ -742,6 +742,68 @@ public class MyClass
await VerifyAgainstBaselineUsingFile("Primitives.generated.txt", source);
}
+
+ [Fact]
+ public async Task DefaultConstructorParameters()
+ {
+ string source = """
+ using System;
+ using System.Globalization;
+ using Microsoft.Extensions.Configuration;
+
+ public class Program
+ {
+ public static void Main()
+ {
+ ConfigurationBuilder configurationBuilder = new();
+ IConfigurationRoot config = configurationBuilder.Build();
+
+ ClassWhereParametersHaveDefaultValue obj = new(default, "");
+ config.Bind(obj);
+ }
+
+ public class ClassWhereParametersHaveDefaultValue
+ {
+ public string? Name { get; }
+ public string Address { get; }
+ public int Age { get; }
+ public float F { get; }
+ public double D { get; }
+ public decimal M { get; }
+ public StringComparison SC { get; }
+ public char C { get; }
+ public int? NAge { get; }
+ public float? NF { get; }
+ public double? ND { get; }
+ public decimal? NM { get; }
+ public StringComparison? NSC { get; }
+ public char? NC { get; }
+
+ public ClassWhereParametersHaveDefaultValue(string? name = "John Doe", string address = "1 Microsoft Way",
+ int age = 42, float f = 42.0f, double d = 3.14159, decimal m = 3.1415926535897932384626433M, StringComparison sc = StringComparison.Ordinal, char c = 'q',
+ int? nage = 42, float? nf = 42.0f, double? nd = 3.14159, decimal? nm = 3.1415926535897932384626433M, StringComparison? nsc = StringComparison.Ordinal, char? nc = 'q')
+ {
+ Name = name;
+ Address = address;
+ Age = age;
+ F = f;
+ D = d;
+ M = m;
+ SC = sc;
+ C = c;
+ NAge = nage;
+ NF = nf;
+ ND = nd;
+ NM = nm;
+ NSC = nsc;
+ NC = nc;
+ }
+ }
+ }
+ """;
+
+ await VerifyAgainstBaselineUsingFile("DefaultConstructorParameters.generated.txt", source);
+ }
[Fact]
public async Task Collections()
diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
index 5109227a9b12d6..90575721c507d7 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
@@ -707,7 +707,7 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin
ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}),
Position = {{spec.ParameterIndex}},
HasDefaultValue = {{FormatBool(spec.HasDefaultValue)}},
- DefaultValue = {{FormatDefaultConstructorParameter(spec.DefaultValue, spec.ParameterType)}}
+ DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}}
};
""");
@@ -1351,56 +1351,6 @@ private static string FormatJsonSerializerDefaults(JsonSerializerDefaults defaul
private static string CreateTypeInfoMethodName(TypeGenerationSpec typeSpec)
=> $"Create_{typeSpec.TypeInfoPropertyName}";
- private static string FormatDefaultConstructorParameter(object? value, TypeRef type)
- {
- if (value == null)
- {
- return $"default({type.FullyQualifiedName})";
- }
-
- if (type.TypeKind is TypeKind.Enum)
- {
- // Return the numeric value.
- return FormatNumber();
- }
-
- switch (value)
- {
- case string @string:
- return SymbolDisplay.FormatLiteral(@string, quote: true); ;
- case char @char:
- return SymbolDisplay.FormatLiteral(@char, quote: true);
- case double.NegativeInfinity:
- return "double.NegativeInfinity";
- case double.PositiveInfinity:
- return "double.PositiveInfinity";
- case double.NaN:
- return "double.NaN";
- case double @double:
- return $"({type.FullyQualifiedName})({@double.ToString(JsonConstants.DoubleFormatString, CultureInfo.InvariantCulture)})";
- case float.NegativeInfinity:
- return "float.NegativeInfinity";
- case float.PositiveInfinity:
- return "float.PositiveInfinity";
- case float.NaN:
- return "float.NaN";
- case float @float:
- return $"({type.FullyQualifiedName})({@float.ToString(JsonConstants.SingleFormatString, CultureInfo.InvariantCulture)})";
- case decimal.MaxValue:
- return "decimal.MaxValue";
- case decimal.MinValue:
- return "decimal.MinValue";
- case decimal @decimal:
- return @decimal.ToString(CultureInfo.InvariantCulture);
- case bool @bool:
- return FormatBool(@bool);
- default:
- // Assume this is a number.
- return FormatNumber();
- }
-
- string FormatNumber() => $"({type.FullyQualifiedName})({Convert.ToString(value, CultureInfo.InvariantCulture)})";
- }
private static string FormatDefaultConstructorExpr(TypeGenerationSpec typeSpec)
{
diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets
index 23add6278d7c07..9b6b9fc77aea52 100644
--- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets
+++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets
@@ -30,6 +30,7 @@
+
diff --git a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs
index b06f0ad9bbae16..0cf16797cd463f 100644
--- a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs
+++ b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs
@@ -1602,6 +1602,14 @@ public class ClassWithIgnoredPropertyDefaultParam
public ClassWithIgnoredPropertyDefaultParam(int x, int y = 5) => (X, Y) = (x, y);
}
+ [Fact]
+ public async Task TestClassWithManyDefaultParams()
+ {
+ string json = "{}";
+ Class_With_Parameters_Default_Values result = await Serializer.DeserializeWrapper(json);
+ result.Verify();
+ }
+
[Fact]
public async Task TestClassWithCustomConverterOnCtorParameter_ShouldPassCorrectTypeToConvertParameter()
{
diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs
index f12cf89e41de59..c2bf9e06a9a335 100644
--- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs
+++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -1972,6 +1973,61 @@ public void VerifyMinimal()
MyTuple.Item4.Verify();
}
}
+ public class Class_With_Parameters_Default_Values
+ {
+ public int I { get; }
+ public float F { get; }
+ public double D { get; }
+ public decimal M { get; }
+ public StringComparison SC { get; }
+ public char C { get; }
+ public int? NI { get; }
+ public float? NF { get; }
+ public double? ND { get; }
+ public decimal? NM { get; }
+ public StringComparison? NSC { get; }
+ public char? NC { get; }
+
+ public Class_With_Parameters_Default_Values(
+ int i = 21, float f = 42.0f, double d = 3.14159, decimal m = 3.1415926535897932384626433M, StringComparison sc = StringComparison.Ordinal, char c = 'q',
+ int? ni = 21, float? nf = 42.0f, double? nd = 3.14159, decimal? nm = 3.1415926535897932384626433M, StringComparison? nsc = StringComparison.Ordinal, char? nc = 'q')
+ {
+ I = i;
+ F = f;
+ D = d;
+ M = m;
+ SC = sc;
+ C = c;
+ NI = ni;
+ NF = nf;
+ ND = nd;
+ NM = nm;
+ NSC = nsc;
+ NC = nc;
+ }
+
+ public void Initialize() { }
+
+ public static readonly string s_json = @"{}";
+
+ public static readonly byte[] s_data = Encoding.UTF8.GetBytes(s_json);
+
+ public void Verify()
+ {
+ Assert.Equal(21, I);
+ Assert.Equal(42.0f, F);
+ Assert.Equal(3.14159, D);
+ Assert.Equal(3.1415926535897932384626433M, M);
+ Assert.Equal(StringComparison.Ordinal, SC);
+ Assert.Equal('q', C);
+ Assert.Equal(21, NI);
+ Assert.Equal(42.0f, NF);
+ Assert.Equal(3.14159, ND);
+ Assert.Equal(3.1415926535897932384626433M, NM);
+ Assert.Equal(StringComparison.Ordinal, NSC);
+ Assert.Equal('q', NC);
+ }
+ }
public class Point_MembersHave_JsonPropertyName : ITestClass
{
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs
index d1769f491104db..1f2fe901f456ea 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs
@@ -112,6 +112,7 @@ protected ConstructorTests_Metadata(JsonSerializerWrapper stringWrapper)
[JsonSerializable(typeof(SimpleClassWithParameterizedCtor_Derived_GenericIDictionary_ObjectExt))]
[JsonSerializable(typeof(Point_MembersHave_JsonInclude))]
[JsonSerializable(typeof(ClassWithFiveArgs_MembersHave_JsonNumberHandlingAttributes))]
+ [JsonSerializable(typeof(Class_With_Parameters_Default_Values))]
[JsonSerializable(typeof(Point_MembersHave_JsonPropertyName))]
[JsonSerializable(typeof(Point_MembersHave_JsonConverter))]
[JsonSerializable(typeof(Point_MembersHave_JsonIgnore))]
@@ -260,6 +261,7 @@ public ConstructorTests_Default(JsonSerializerWrapper jsonSerializer) : base(jso
[JsonSerializable(typeof(SimpleClassWithParameterizedCtor_Derived_GenericIDictionary_ObjectExt))]
[JsonSerializable(typeof(Point_MembersHave_JsonInclude))]
[JsonSerializable(typeof(ClassWithFiveArgs_MembersHave_JsonNumberHandlingAttributes))]
+ [JsonSerializable(typeof(Class_With_Parameters_Default_Values))]
[JsonSerializable(typeof(Point_MembersHave_JsonPropertyName))]
[JsonSerializable(typeof(Point_MembersHave_JsonConverter))]
[JsonSerializable(typeof(Point_MembersHave_JsonIgnore))]