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))]