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 d01c5dbae13f3c..3b5d460087751d 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs
@@ -583,9 +583,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
};
@@ -617,9 +616,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/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj
index b66741549b4584..d3fa37da6a2f6d 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
@@ -28,6 +28,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..cc34f80055dc83 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,20 +112,45 @@ 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, string address,
+ 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;
}
}
+
public class ClassWithPrimaryCtor(string color, int length)
{
public string Color { get; } = color;
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..ab372b8ec814ee 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]
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))]