From 769ff35f9b35225ec8627a75f25a20607e541baa Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Mon, 15 May 2023 16:45:49 -0700 Subject: [PATCH 1/3] Add support for types with parameterized ctors to config binder gen --- .../ConfigurationBindingGenerator.Emitter.cs | 151 ++++++++++++++++-- .../ConfigurationBindingGenerator.Parser.cs | 119 +++++++++++--- ...nfiguration.Binder.SourceGeneration.csproj | 11 +- .../gen/Model/BinderMethodSpecifier.cs | 1 + .../gen/Model/ObjectSpec.cs | 11 +- .../gen/Model/ParameterSpec.cs | 39 +++++ .../gen/Model/SourceGenerationSpec.cs | 3 +- .../gen/Model/TypeSpec.cs | 2 +- .../tests/Common/ConfigurationBinderTests.cs | 44 ++--- .../Baselines/TestBindCallGen.generated.txt | 4 +- .../TestCollectionsGen.generated.txt | 4 +- .../TestConfigureCallGen.generated.txt | 4 +- .../Baselines/TestGetCallGen.generated.txt | 4 +- .../TestGetValueCallGen.generated.txt | 3 +- .../Baselines/TestPrimitivesGen.generated.txt | 3 +- 15 files changed, 321 insertions(+), 82 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs index 7fa5cf315e46b4..1023204a8827d4 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs @@ -72,6 +72,7 @@ public void Emit() EmitGetCoreMethod(); EmitGetValueCoreMethod(); EmitBindCoreMethods(); + EmitInstantiateMethods(); EmitHelperMethods(); _writer.WriteBlockEnd(); // End helper class. @@ -328,6 +329,116 @@ private void EmitBindCoreMethod(TypeSpec type) _writer.WriteBlockEnd(); } + private void EmitInstantiateMethods() + { + if (!_generationSpec.ShouldEmitMethods(BinderMethodSpecifier.Instantiate)) + { + return; + } + + foreach (ObjectSpec type in _generationSpec.RootConfigTypes[BinderMethodSpecifier.Instantiate]) + { + EmitBlankLineIfRequired(); + EmitInstantiateMethod(type); + } + } + + private void EmitInstantiateMethod(ObjectSpec type) + { + Debug.Assert(type.ConstructorParameters.Count > 0); + + string displayString = type.MinimalDisplayString; + + _writer.WriteBlockStart($"public static {displayString} {type.InstantiateMethodName}({Identifier.IConfiguration} {Identifier.configuration}, {Identifier.BinderOptions}? {Identifier.binderOptions})"); + + foreach (ParameterSpec parameter in type.ConstructorParameters) + { + _writer.WriteLine($@"{parameter.Type.MinimalDisplayString} {parameter.Name} = {parameter.DefaultValue};"); + } + + foreach (PropertySpec property in type.InitOnlyProperties) + { + _writer.WriteLine($@"{property.Type.MinimalDisplayString} {property.Name} = default!;"); + } + + _writer.WriteBlankLine(); + + _writer.WriteBlock($$""" + foreach ({{Identifier.IConfigurationSection}} {{Identifier.section}} in {{Identifier.configuration}}.{{Identifier.GetChildren}}()) + { + switch ({{Expression.sectionKey}}) + { + """); + + List argumentList = new(); + + foreach (ParameterSpec parameter in type.ConstructorParameters) + { + TypeSpec parameterType = parameter.Type; + string parameterName = parameter.Name; + string configurationKeyName = parameter.MatchingProperty?.ConfigurationKeyName ?? parameterName; + + EmitMemberBindLogic(parameterName, parameterType, configurationKeyName); + argumentList.Add(parameter.GetExpressionForArgument(parameterName)); + } + + foreach (PropertySpec property in type.InitOnlyProperties) + { + if (property.ShouldBind()) + { + EmitMemberBindLogic(property.Name, property.Type, property.ConfigurationKeyName); + } + } + + _writer.WriteBlock(""" + default: + { + continue; + } + } + } + """); + + _writer.WriteBlankLine(); + _writer.WriteBlockStart($"return new {displayString}({string.Join(", ", argumentList)})"); + foreach (PropertySpec property in type.InitOnlyProperties) + { + _writer.WriteLine($@"{property.Name} = {property.Name},"); + } + _writer.WriteBlockEnd(";"); + + // End method. + _writer.WriteBlockEnd(); + + #region Local helpers + void EmitMemberBindLogic(string memberName, TypeSpec memberType, string configurationKeyName) + { + _writer.WriteBlockStart($@"case ""{configurationKeyName}"":"); + EmitMemberBindLogicCore(memberType, varName: memberName); + _writer.WriteBlockEnd(); + _writer.WriteLine("break;"); + + void EmitMemberBindLogicCore(TypeSpec type, string varName) + { + TypeSpecKind kind = type.SpecKind; + + if (kind is TypeSpecKind.Nullable) + { + EmitMemberBindLogicCore(((NullableSpec)type).UnderlyingType, varName); + } + else if (kind is TypeSpecKind.ParsableFromString) + { + EmitBindLogicFromString((ParsableFromStringSpec)type, varName, Expression.sectionValue, Expression.sectionPath); + } + else + { + EmitBindCoreCall(type, varName, Identifier.section, InitializationKind.SimpleAssignment); + } + } + } + #endregion + } + private void EmitHelperMethods() { if (_generationSpec.ShouldEmitMethods(BinderMethodSpecifier.Get | BinderMethodSpecifier.Configure)) @@ -712,7 +823,7 @@ void Emit_BindAndAddLogic_ForElement() private void EmitBindCoreImplForObject(ObjectSpec type) { - Dictionary properties = type.Properties; + Dictionary properties = type.PropertiesBindableAfterInit; if (properties.Count == 0) { return; @@ -978,24 +1089,28 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini string expressionForInit; CollectionSpec? collectionType = type as CollectionSpec; - string typeDisplayString; + string effectiveDisplayString = GetTypeDisplayString(type); if (collectionType is not null) { if (collectionType is EnumerableSpec { PopulationStrategy: CollectionPopulationStrategy.Array }) { - typeDisplayString = GetTypeDisplayString(type); - expressionForInit = $"new {_arrayBracketsRegex.Replace(typeDisplayString, "[0]", 1)}"; + expressionForInit = $"new {_arrayBracketsRegex.Replace(effectiveDisplayString, "[0]", 1)}"; } else { - typeDisplayString = GetTypeDisplayString(collectionType.ConcreteType ?? collectionType); - expressionForInit = $"new {typeDisplayString}()"; + effectiveDisplayString = GetTypeDisplayString(collectionType.ConcreteType ?? collectionType); + expressionForInit = $"new {effectiveDisplayString}()"; } } else if (type.ConstructionStrategy is ConstructionStrategy.ParameterlessConstructor) { - typeDisplayString = GetTypeDisplayString(type); - expressionForInit = $"new {typeDisplayString}()"; + expressionForInit = $"new {effectiveDisplayString}()"; + } + else if (type.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor) + { + string expressionForConfigSection = initKind is InitializationKind.Declaration ? Identifier.configuration : Identifier.section; + string identifierForInstantiateMethod = GetHelperMethodDisplayString(((ObjectSpec)type).InstantiateMethodName); + expressionForInit = $"{identifierForInstantiateMethod}({expressionForConfigSection}, {Identifier.binderOptions});"; } else { @@ -1009,14 +1124,19 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini } else if (initKind == InitializationKind.AssignmentWithNullCheck) { - ConstructionStrategy? collectionConstructionStratey = collectionType?.ConstructionStrategy; - if (collectionConstructionStratey is ConstructionStrategy.ParameterizedConstructor) - { - _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : new {typeDisplayString}({expressionForMemberAccess});"); - } - else if (collectionConstructionStratey is ConstructionStrategy.ToEnumerableMethod) + if (collectionType is CollectionSpec + { + ConstructionStrategy: ConstructionStrategy.ParameterizedConstructor or ConstructionStrategy.ToEnumerableMethod + }) { - _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : {expressionForMemberAccess}.{collectionType.ToEnumerableMethodCall!};"); + if (collectionType.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor) + { + _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : new {effectiveDisplayString}({expressionForMemberAccess});"); + } + else + { + _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : {expressionForMemberAccess}.{collectionType.ToEnumerableMethodCall!};"); + } } else { @@ -1025,6 +1145,7 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini } else { + Debug.Assert(initKind is InitializationKind.SimpleAssignment); _writer.WriteLine($"{expressionForMemberAccess} = {expressionForInit};"); } } 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 c1678248f0a19a..6511f66d669a2f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs @@ -28,6 +28,7 @@ private sealed class Parser private readonly HashSet _typeNamespaces = new() { "System", + "System.Collections.Generic", "System.Globalization", "Microsoft.Extensions.Configuration" }; @@ -86,7 +87,7 @@ public Parser(SourceProductionContext context, KnownTypeSymbols typeSymbols) _rootConfigTypes, _methodsToGen, _primitivesForHelperGen, - _typeNamespaces); + _typeNamespaces.ToImmutableSortedSet()); } private void ProcessBindCall(BinderInvocationOperation binderOperation) @@ -329,9 +330,7 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) if (spec != null) { AddToRootConfigTypeCache(overload, spec); - AddToRootConfigTypeCache(methodGroup, spec); - - _methodsToGen |= overload; + AddToRootConfigTypeCache(methodGroup, spec, registerAsMethodToGen: false); } return spec; @@ -405,12 +404,11 @@ void RegisterBindCoreGenType(TypeSpec? spec) if (spec is not null) { AddToRootConfigTypeCache(BinderMethodSpecifier.BindCore, spec); - _methodsToGen |= BinderMethodSpecifier.BindCore; } } } - private void AddToRootConfigTypeCache(BinderMethodSpecifier method, TypeSpec spec) + private void AddToRootConfigTypeCache(BinderMethodSpecifier method, TypeSpec spec, bool registerAsMethodToGen = true) { Debug.Assert(spec is not null); @@ -420,6 +418,11 @@ private void AddToRootConfigTypeCache(BinderMethodSpecifier method, TypeSpec spe } types.Add(spec); + + if (registerAsMethodToGen) + { + _methodsToGen |= method; + } } private static bool IsNullable(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? underlyingType) @@ -625,7 +628,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc INamedTypeSymbol? populationCastType = null; string? toEnumerableMethodCall = null; - if (HasPublicParameterlessCtor(type)) + if (HasPublicParameterLessCtor(type)) { constructionStrategy = ConstructionStrategy.ParameterlessConstructor; @@ -695,7 +698,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc INamedTypeSymbol? concreteType = null; INamedTypeSymbol? populationCastType = null; - if (HasPublicParameterlessCtor(type)) + if (HasPublicParameterLessCtor(type)) { constructionStrategy = ConstructionStrategy.ParameterlessConstructor; @@ -776,16 +779,41 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc { Debug.Assert(!_createdSpecs.ContainsKey(type)); + IMethodSymbol? ctor = GetConstructor(type); + // Add spec to cache before traversing properties to avoid stack overflow. - if (!HasPublicParameterlessCtor(type)) + + if (ctor is null) { ReportUnsupportedType(type, ParserDiagnostics.NeedPublicParameterlessConstructor, location); _createdSpecs.Add(type, null); return null; } - ObjectSpec objectSpec = new(type) { Location = location, ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor }; + + ConstructionStrategy constructionStrategy = ctor.Parameters.Length is 0 ? ConstructionStrategy.ParameterlessConstructor : ConstructionStrategy.ParameterizedConstructor; + + ObjectSpec objectSpec = new(type) + { + Location = location, + ConstructionStrategy = constructionStrategy, + }; + _createdSpecs.Add(type, objectSpec); + Dictionary? parameterCache = null; + if (constructionStrategy is ConstructionStrategy.ParameterizedConstructor) + { + AddToRootConfigTypeCache(BinderMethodSpecifier.Instantiate, objectSpec); + + parameterCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (IParameterSymbol parameter in ctor.Parameters) + { + ParameterSpec parameterSpec = new ParameterSpec(parameter) { Type = GetOrCreateTypeSpec(parameter.Type) }; + parameterCache[parameter.Name] = parameterSpec; + objectSpec.ConstructorParameters.Add(parameterSpec); + } + } + INamedTypeSymbol current = type; while (current is not null) { @@ -800,8 +828,6 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName; TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(propertyType); - PropertySpec spec; - if (propertyTypeSpec is null) { _context.ReportDiagnostic(Diagnostic.Create(ParserDiagnostics.PropertyNotSupported, location, new string[] { propertyName, type.ToDisplayString() })); @@ -811,9 +837,26 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc RegisterHasChildrenHelperForGenIfRequired(propertyTypeSpec); } + PropertySpec spec = new(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; - spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; - objectSpec.Properties[configKeyName] = spec; + ParameterSpec? matchingParam = null; + if ((parameterCache?.TryGetValue(propertyName, out matchingParam)) is true && + matchingParam.Type == propertyTypeSpec) + { + matchingParam.MatchingProperty = spec; + } + + if (property.IsRequired || property.SetMethod?.IsInitOnly is true) + { + if (matchingParam is null) + { + objectSpec.InitOnlyProperties.Add(spec); + } + } + else + { + objectSpec.PropertiesBindableAfterInit[configKeyName] = spec; + } } } } @@ -921,29 +964,55 @@ public static bool ContainsGenericParameters(INamedTypeSymbol type) return false; } - private static bool HasPublicParameterlessCtor(INamedTypeSymbol type) + private static bool HasPublicParameterLessCtor(INamedTypeSymbol type) => + CannotBeConstructed(type) + ? false + : type.InstanceConstructors.SingleOrDefault(ctor => ctor.DeclaredAccessibility is Accessibility.Public && ctor.Parameters.Length is 0) + is not null; + + private static IMethodSymbol? GetConstructor(INamedTypeSymbol type) { - if (type.IsAbstract || type.TypeKind == TypeKind.Interface) + if (CannotBeConstructed(type)) { - return false; + return null; } - if (type is not INamedTypeSymbol namedType) - { - return false; - } + IMethodSymbol? publicParameterlessCtor = null; + IMethodSymbol? publicParameterizedCtor = null; + bool hasMultiplePublicParameterizedCtors = false; - foreach (IMethodSymbol ctor in namedType.InstanceConstructors) + foreach (IMethodSymbol ctor in type.InstanceConstructors) { - if (ctor.DeclaredAccessibility == Accessibility.Public && ctor.Parameters.Length == 0) + if (ctor.DeclaredAccessibility is not Accessibility.Public) { - return true; + continue; + } + + if (ctor.Parameters.Length is 0) + { + publicParameterlessCtor = ctor; + } + else + { + if (publicParameterizedCtor is not null) + { + hasMultiplePublicParameterizedCtors = true; + } + + publicParameterizedCtor = ctor; } } - return false; + if (publicParameterlessCtor is null && hasMultiplePublicParameterizedCtors) + { + return null; + } + + return publicParameterizedCtor ?? publicParameterlessCtor; } + private static bool CannotBeConstructed(INamedTypeSymbol type) => type.IsAbstract || type.TypeKind == TypeKind.Interface; + private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol element) { INamedTypeSymbol current = type; 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 2f2e29690f4fd0..0a3786607c66d3 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 @@ -23,22 +23,23 @@ - - + + + - - - + + + diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/BinderMethodSpecifier.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/BinderMethodSpecifier.cs index 37a39f71014b95..a34b5818f34e18 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/BinderMethodSpecifier.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/BinderMethodSpecifier.cs @@ -74,6 +74,7 @@ internal enum BinderMethodSpecifier // Binding helpers BindCore = 0x1000, HasChildren = 0x4000, + Instantiate = 0x8000, // Method groups Bind = Bind_instance | Bind_instance_BinderOptions | Bind_key_instance, diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs index 046d521a087225..f81da45f284e8a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs @@ -8,10 +8,15 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { internal sealed record ObjectSpec : TypeSpec { - public ObjectSpec(INamedTypeSymbol type) : base(type) { } + public ObjectSpec(INamedTypeSymbol type) : base(type) + { + InstantiateMethodName = $"Initialize{MinimalDisplayString.Replace(".", string.Empty).Replace("<", string.Empty).Replace(">", string.Empty)}"; + } public override TypeSpecKind SpecKind => TypeSpecKind.Object; - - public Dictionary Properties { get; } = new(); + public List ConstructorParameters { get; } = new(); + public Dictionary PropertiesBindableAfterInit { get; } = new(); + public List InitOnlyProperties { get; } = new(); + public string InstantiateMethodName { get; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs new file mode 100644 index 00000000000000..f4bd6c9d46fef6 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + internal sealed record ParameterSpec + { + public required TypeSpec? Type { get; init; } + public string Name { get; } + public PropertySpec? MatchingProperty { get; set; } + public RefKind RefKind { get; } + public string DefaultValue { get; } = "default!"; + + public ParameterSpec(IParameterSymbol parameter) + { + Name = parameter.Name; + RefKind = parameter.RefKind; + + if (parameter.HasExplicitDefaultValue) + { + string formatted = SymbolDisplay.FormatPrimitive(parameter.ExplicitDefaultValue, quoteStrings: true, useHexadecimalNumbers: false); + DefaultValue = formatted is "null" ? "default!" : formatted; + } + } + + public string GetExpressionForArgument(string argument) => RefKind switch + { + RefKind.None => argument, + RefKind.Ref => $"ref {argument}", + RefKind.Out => "out _", + RefKind.In => $"in {argument}", + _ => throw new InvalidOperationException("Unknown ref kind") + }; + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs index db8e1aaccce1c5..db88141e551f33 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Collections.Immutable; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { @@ -9,7 +10,7 @@ internal sealed record SourceGenerationSpec( Dictionary> RootConfigTypes, BinderMethodSpecifier MethodsToGen, HashSet PrimitivesForHelperGen, - HashSet TypeNamespaces) + ImmutableSortedSet TypeNamespaces) { public bool HasRootMethods() => ShouldEmitMethods(BinderMethodSpecifier.Get | BinderMethodSpecifier.Bind | BinderMethodSpecifier.Configure | BinderMethodSpecifier.GetValue); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs index 3b416736fefdee..b7a05abae21cc5 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs @@ -31,7 +31,7 @@ public TypeSpec(ITypeSymbol type) public abstract TypeSpecKind SpecKind { get; } - public virtual ConstructionStrategy ConstructionStrategy { get; init; } + public virtual ConstructionStrategy ConstructionStrategy { get; set; } /// /// Where in the input compilation we picked up a call to Bind, Get, or Configure. 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 c84d33bf8a4d2c..afb6cb5de33987 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -19,7 +19,7 @@ namespace Microsoft.Extensions { public partial class ConfigurationBinderTests { - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for records. + [Fact] public void BindWithNestedTypesWithReadOnlyProperties() { IConfiguration configuration = new ConfigurationBuilder() @@ -920,7 +920,7 @@ public void ExceptionWhenTryingToBindClassWithoutParameterlessConstructor() exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParameters() { var input = new Dictionary @@ -941,7 +941,7 @@ public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParam exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() { var input = new Dictionary @@ -961,7 +961,7 @@ public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor() { var input = new Dictionary @@ -982,7 +982,7 @@ public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoun exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void BindsToClassConstructorParametersWithDefaultValues() { var input = new Dictionary @@ -1003,7 +1003,7 @@ public void BindsToClassConstructorParametersWithDefaultValues() Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.Age); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void FieldsNotSupported_ExceptionBindingToConstructorWithParameterMatchingAField() { var input = new Dictionary @@ -1025,7 +1025,7 @@ public void FieldsNotSupported_ExceptionBindingToConstructorWithParameterMatchin exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void BindsToRecordPrimaryConstructorParametersWithDefaultValues() { var input = new Dictionary @@ -1101,7 +1101,7 @@ public void CanBindValueTypeOptions() Assert.Equal("hello world", options.MyString); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindImmutableClass() { var dic = new Dictionary @@ -1118,7 +1118,7 @@ public void CanBindImmutableClass() Assert.Equal("Green", options.Color); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindMutableClassWitNestedImmutableObject() { var dic = new Dictionary @@ -1139,7 +1139,7 @@ public void CanBindMutableClassWitNestedImmutableObject() // If the immutable type has multiple public parameterized constructors, then throw // an exception. - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void CanBindImmutableClass_ThrowsOnMultipleParameterizedConstructors() { var dic = new Dictionary @@ -1162,7 +1162,7 @@ public void CanBindImmutableClass_ThrowsOnMultipleParameterizedConstructors() // If the immutable type has a parameterized constructor, then throw // that constructor has an 'in' parameter - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnInParameter() { var dic = new Dictionary @@ -1185,7 +1185,7 @@ public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnInParame // If the immutable type has a parameterized constructors, then throw // that constructor has a 'ref' parameter - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithARefParameter() { var dic = new Dictionary @@ -1208,7 +1208,7 @@ public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithARefParame // If the immutable type has a parameterized constructors, then throw // if the constructor has an 'out' parameter - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnOutParameter() { var dic = new Dictionary @@ -1229,7 +1229,7 @@ public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnOutParam Assert.Equal(expectedMessage, ex.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]// Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void CanBindMutableStruct_UnmatchedConstructorsAreIgnored() { var dic = new Dictionary @@ -1248,7 +1248,7 @@ public void CanBindMutableStruct_UnmatchedConstructorsAreIgnored() // If the immutable type has a public parameterized constructor, // then pick it. - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindImmutableClass_PicksParameterizedConstructorIfNoParameterlessConstructorExists() { var dic = new Dictionary @@ -1269,7 +1269,7 @@ public void CanBindImmutableClass_PicksParameterizedConstructorIfNoParameterless Assert.Equal(2, options.Int2); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindSemiImmutableClass() { var dic = new Dictionary @@ -1288,7 +1288,7 @@ public void CanBindSemiImmutableClass() Assert.Equal(1.23m, options.Thickness); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindSemiImmutableClass_WithInitProperties() { var dic = new Dictionary @@ -1307,7 +1307,7 @@ public void CanBindSemiImmutableClass_WithInitProperties() Assert.Equal(1.23m, options.Thickness); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindRecordOptions() { var dic = new Dictionary @@ -1324,7 +1324,7 @@ public void CanBindRecordOptions() Assert.Equal("Green", options.Color); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindRecordStructOptions() { var dic = new Dictionary @@ -1341,7 +1341,7 @@ public void CanBindRecordStructOptions() Assert.Equal("Green", options.Color); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindNestedRecordOptions() { var dic = new Dictionary @@ -1364,7 +1364,7 @@ public void CanBindNestedRecordOptions() Assert.Equal(24, options.Nested2.ValueB); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. public void CanBindOnParametersAndProperties_PropertiesAreSetAfterTheConstructor() { var dic = new Dictionary @@ -1381,7 +1381,7 @@ public void CanBindOnParametersAndProperties_PropertiesAreSetAfterTheConstructor Assert.Equal("the color is Green", options.Color); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindReadonlyRecordStructOptions() { var dic = new Dictionary diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt index b18fa8476a80a9..5c7c635dac9ace 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt @@ -12,10 +12,10 @@ internal static class GeneratedConfigurationBinder namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { - using System; - using System.Globalization; using Microsoft.Extensions.Configuration; + using System; using System.Collections.Generic; + using System.Globalization; internal static class Helpers { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt index cba992ebfc1bb6..e8508b3946c6ff 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt @@ -8,10 +8,10 @@ internal static class GeneratedConfigurationBinder namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { - using System; - using System.Globalization; using Microsoft.Extensions.Configuration; + using System; using System.Collections.Generic; + using System.Globalization; using System.Linq; internal static class Helpers diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt index fb0621d9668dc6..e0bfd544e860b6 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt @@ -29,10 +29,10 @@ internal static class GeneratedConfigurationBinder namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { - using System; - using System.Globalization; using Microsoft.Extensions.Configuration; + using System; using System.Collections.Generic; + using System.Globalization; internal static class Helpers { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt index 882ff2d8a8f416..c9c2c5626e143c 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt @@ -14,10 +14,10 @@ internal static class GeneratedConfigurationBinder namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { - using System; - using System.Globalization; using Microsoft.Extensions.Configuration; + using System; using System.Collections.Generic; + using System.Globalization; internal static class Helpers { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetValueCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetValueCallGen.generated.txt index cbf8e6f3ef1b5d..ed3c062c6729de 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetValueCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetValueCallGen.generated.txt @@ -14,9 +14,10 @@ internal static class GeneratedConfigurationBinder namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { + using Microsoft.Extensions.Configuration; using System; + using System.Collections.Generic; using System.Globalization; - using Microsoft.Extensions.Configuration; internal static class Helpers { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestPrimitivesGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestPrimitivesGen.generated.txt index 0f0a4930270c8f..7972cab29c16b2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestPrimitivesGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestPrimitivesGen.generated.txt @@ -8,9 +8,10 @@ internal static class GeneratedConfigurationBinder namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { + using Microsoft.Extensions.Configuration; using System; + using System.Collections.Generic; using System.Globalization; - using Microsoft.Extensions.Configuration; internal static class Helpers { From 3fd46fd89679567e9d31ee3e9e2c6d37f4d0c9fe Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Wed, 17 May 2023 13:36:18 -0700 Subject: [PATCH 2/3] Ensure param-prop binding & exception behavior match reflection implementation --- .../ConfigurationBindingGenerator.Emitter.cs | 358 +++++++++++------- .../ConfigurationBindingGenerator.Parser.cs | 341 +++++++++-------- .../gen/Helpers/Emitter.Helpers.cs | 38 +- .../gen/Helpers/ExceptionMessages.cs | 6 + .../gen/Helpers/ParserDiagnostics.cs | 3 +- ...nfiguration.Binder.SourceGeneration.csproj | 2 +- .../gen/Model/BinderMethodSpecifier.cs | 2 +- .../gen/Model/CollectionSpec.cs | 11 +- ...nStrategy.cs => InitializationStrategy.cs} | 3 +- .../gen/Model/NullableSpec.cs | 7 + .../gen/Model/ObjectSpec.cs | 15 +- .../gen/Model/ParameterSpec.cs | 29 +- .../gen/Model/PropertySpec.cs | 24 +- .../gen/Model/SourceGenerationSpec.cs | 2 +- .../gen/Model/TypeSpec.cs | 26 +- .../gen/Resources/Strings.resx | 7 +- .../gen/Resources/xlf/Strings.cs.xlf | 11 +- .../gen/Resources/xlf/Strings.de.xlf | 11 +- .../gen/Resources/xlf/Strings.es.xlf | 11 +- .../gen/Resources/xlf/Strings.fr.xlf | 11 +- .../gen/Resources/xlf/Strings.it.xlf | 11 +- .../gen/Resources/xlf/Strings.ja.xlf | 11 +- .../gen/Resources/xlf/Strings.ko.xlf | 11 +- .../gen/Resources/xlf/Strings.pl.xlf | 11 +- .../gen/Resources/xlf/Strings.pt-BR.xlf | 11 +- .../gen/Resources/xlf/Strings.ru.xlf | 11 +- .../gen/Resources/xlf/Strings.tr.xlf | 11 +- .../gen/Resources/xlf/Strings.zh-Hans.xlf | 11 +- .../gen/Resources/xlf/Strings.zh-Hant.xlf | 11 +- .../ConfigurationBinderTests.TestClasses.cs | 6 +- .../tests/Common/ConfigurationBinderTests.cs | 51 ++- .../Baselines/TestBindCallGen.generated.txt | 13 +- .../TestCollectionsGen.generated.txt | 18 +- .../Baselines/TestGetCallGen.generated.txt | 18 +- 34 files changed, 695 insertions(+), 428 deletions(-) rename src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/{ConstructionStrategy.cs => InitializationStrategy.cs} (85%) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs index 1023204a8827d4..f309f203486bf7 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; @@ -72,7 +73,7 @@ public void Emit() EmitGetCoreMethod(); EmitGetValueCoreMethod(); EmitBindCoreMethods(); - EmitInstantiateMethods(); + EmitInitializeMethods(); EmitHelperMethods(); _writer.WriteBlockEnd(); // End helper class. @@ -94,7 +95,7 @@ private void EmitConfigureMethod() EmitCheckForNullArgument_WithBlankLine(Identifier.configuration, useFullyQualifiedNames: true); - foreach (TypeSpec type in _generationSpec.RootConfigTypes[BinderMethodSpecifier.Configure]) + foreach (TypeSpec type in _generationSpec.ConfigTypes[BinderMethodSpecifier.Configure]) { string typeDisplayString = type.FullyQualifiedDisplayString; @@ -122,28 +123,28 @@ private void EmitGetMethods() { EmitBlankLineIfRequired(); _writer.WriteLine($"public static T? {Identifier.Get}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}) => " + - $"(T?)({expressionForGetCore}({Identifier.configuration}, typeof(T), {Identifier.configureActions}: null) ?? default(T));"); + $"(T?)({expressionForGetCore}({Identifier.configuration}, typeof(T), {Identifier.configureOptions}: null) ?? default(T));"); } if (_generationSpec.ShouldEmitMethods(BinderMethodSpecifier.Get_T_BinderOptions)) { EmitBlankLineIfRequired(); - _writer.WriteLine($"public static T? {Identifier.Get}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureActions}) => " + - $"(T?)({expressionForGetCore}({Identifier.configuration}, typeof(T), {Identifier.configureActions}) ?? default(T));"); + _writer.WriteLine($"public static T? {Identifier.Get}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureOptions}) => " + + $"(T?)({expressionForGetCore}({Identifier.configuration}, typeof(T), {Identifier.configureOptions}) ?? default(T));"); } if (_generationSpec.ShouldEmitMethods(BinderMethodSpecifier.Get_TypeOf)) { EmitBlankLineIfRequired(); _writer.WriteLine($"public static object? {Identifier.Get}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.Type} {Identifier.type}) => " + - $"{expressionForGetCore}({Identifier.configuration}, {Identifier.type}, {Identifier.configureActions}: null);"); + $"{expressionForGetCore}({Identifier.configuration}, {Identifier.type}, {Identifier.configureOptions}: null);"); } if (_generationSpec.ShouldEmitMethods(BinderMethodSpecifier.Get_TypeOf_BinderOptions)) { EmitBlankLineIfRequired(); - _writer.WriteLine($"public static object? {Identifier.Get}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.Type} {Identifier.type}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureActions}) => " + - $"{expressionForGetCore}({Identifier.configuration}, {Identifier.type}, {Identifier.configureActions});"); + _writer.WriteLine($"public static object? {Identifier.Get}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.Type} {Identifier.type}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureOptions}) => " + + $"{expressionForGetCore}({Identifier.configuration}, {Identifier.type}, {Identifier.configureOptions});"); } } @@ -187,16 +188,17 @@ private void EmitBindMethods() return; } - Dictionary> rootConfigTypes = _generationSpec.RootConfigTypes; + Dictionary> rootConfigTypes = _generationSpec.ConfigTypes; if (rootConfigTypes.TryGetValue(BinderMethodSpecifier.Bind_instance, out HashSet? typeSpecs)) { foreach (TypeSpec type in typeSpecs) { - EmitBlankLineIfRequired(); - _writer.WriteLine( - $"public static void {Identifier.Bind}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {type.FullyQualifiedDisplayString} {Identifier.obj}) => " + - $"{FullyQualifiedDisplayName.Helpers}.{Identifier.BindCore}({Identifier.configuration}, ref {Identifier.obj}, {Identifier.binderOptions}: null);"); + EmitMethodImplementation( + type, + additionalParams: GetObjParameter(type), + configExpression: Identifier.configuration, + configureOptions: false); } } @@ -204,10 +206,11 @@ private void EmitBindMethods() { foreach (TypeSpec type in typeSpecs) { - EmitBlankLineIfRequired(); - _writer.WriteLine( - $"public static void {Identifier.Bind}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {type.FullyQualifiedDisplayString} {Identifier.obj}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureActions}) => " + - $"{FullyQualifiedDisplayName.Helpers}.{Identifier.BindCore}({Identifier.configuration}, ref {Identifier.obj}, {Expression.GetBinderOptions}({Identifier.configureActions}));"); + EmitMethodImplementation( + type, + additionalParams: $"{GetObjParameter(type)}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureOptions}", + configExpression: Identifier.configuration, + configureOptions: true); } } @@ -215,11 +218,27 @@ private void EmitBindMethods() { foreach (TypeSpec type in typeSpecs) { - EmitBlankLineIfRequired(); - _writer.WriteLine($"public static void {Identifier.Bind}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, string {Identifier.key}, {type.FullyQualifiedDisplayString} {Identifier.obj}) => " + - $"{FullyQualifiedDisplayName.Helpers}.{Identifier.BindCore}({Identifier.configuration}.{Identifier.GetSection}({Identifier.key}), ref {Identifier.obj}, {Identifier.binderOptions}: null);"); + EmitMethodImplementation( + type, + additionalParams: $"string {Identifier.key}, {GetObjParameter(type)}", + configExpression: $"{Identifier.configuration}.{Identifier.GetSection}({Identifier.key})", + configureOptions: false); } } + + void EmitMethodImplementation(TypeSpec type, string additionalParams, string configExpression, bool configureOptions) + { + string binderOptionsArg = configureOptions ? $"{Expression.GetBinderOptions}({Identifier.configureOptions})" : $"{Identifier.binderOptions}: null"; + string returnExpression = type.CanInitialize + ? $"{FullyQualifiedDisplayName.Helpers}.{Identifier.BindCore}({configExpression}, ref {Identifier.obj}, {binderOptionsArg})" + : GetInitException(type.InitExceptionMessage); + + EmitBlankLineIfRequired(); + _writer.WriteLine($"public static void {Identifier.Bind}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {additionalParams}) => " + + $"{returnExpression};"); + } + + string GetObjParameter(TypeSpec type) => $"{type.FullyQualifiedDisplayString} {Identifier.obj}"; } #endregion @@ -239,22 +258,27 @@ private void EmitGetCoreMethod() return; } - _writer.WriteBlockStart($"public static object? {Identifier.GetCore}(this {Identifier.IConfiguration} {Identifier.configuration}, Type {Identifier.type}, Action<{Identifier.BinderOptions}>? {Identifier.configureActions})"); + _writer.WriteBlockStart($"public static object? {Identifier.GetCore}(this {Identifier.IConfiguration} {Identifier.configuration}, Type {Identifier.type}, Action<{Identifier.BinderOptions}>? {Identifier.configureOptions})"); EmitCheckForNullArgument_WithBlankLine(Identifier.configuration); - _writer.WriteLine($"{Identifier.BinderOptions}? {Identifier.binderOptions} = {Identifier.GetBinderOptions}({Identifier.configureActions});"); + _writer.WriteLine($"{Identifier.BinderOptions}? {Identifier.binderOptions} = {Identifier.GetBinderOptions}({Identifier.configureOptions});"); _writer.WriteBlankLine(); EmitIConfigurationHasValueOrChildrenCheck(voidReturn: false); - if (_generationSpec.RootConfigTypes.TryGetValue(BinderMethodSpecifier.Get, out HashSet? types)) + if (_generationSpec.ConfigTypes.TryGetValue(BinderMethodSpecifier.Get, out HashSet? types)) { foreach (TypeSpec type in types) { _writer.WriteBlockStart($"if (type == typeof({type.MinimalDisplayString}))"); - EmitBindLogicFromRootMethod(type, Identifier.obj, InitializationKind.Declaration); - _writer.WriteLine($"return {Identifier.obj};"); + + if (type.InitializationStrategy is InitializationStrategy.None || !EmitInitException(type)) + { + EmitBindLogicFromRootMethod(type, Identifier.obj, InitializationKind.Declaration); + _writer.WriteLine($"return {Identifier.obj};"); + } + _writer.WriteBlockEnd(); _writer.WriteBlankLine(); } @@ -283,7 +307,7 @@ private void EmitGetValueCoreMethod() _writer.WriteBlankLine(); - foreach (TypeSpec type in _generationSpec.RootConfigTypes[BinderMethodSpecifier.GetValue]) + foreach (TypeSpec type in _generationSpec.ConfigTypes[BinderMethodSpecifier.GetValue]) { TypeSpec effectiveType = (type as NullableSpec)?.UnderlyingType ?? type; _writer.WriteBlockStart($"if (type == typeof({type.MinimalDisplayString}))"); @@ -309,7 +333,7 @@ private void EmitBindCoreMethods() return; } - foreach (TypeSpec type in _generationSpec.RootConfigTypes[BinderMethodSpecifier.BindCore]) + foreach (TypeSpec type in _generationSpec.ConfigTypes[BinderMethodSpecifier.BindCore]) { if (type.SpecKind is TypeSpecKind.ParsableFromString) { @@ -323,42 +347,56 @@ private void EmitBindCoreMethods() private void EmitBindCoreMethod(TypeSpec type) { + Debug.Assert(type.CanInitialize); + string objParameterExpression = $"ref {type.MinimalDisplayString} {Identifier.obj}"; _writer.WriteBlockStart(@$"public static void {Identifier.BindCore}({Identifier.IConfiguration} {Identifier.configuration}, {objParameterExpression}, {Identifier.BinderOptions}? {Identifier.binderOptions})"); EmitBindCoreImpl(type); _writer.WriteBlockEnd(); } - private void EmitInstantiateMethods() + private void EmitInitializeMethods() { - if (!_generationSpec.ShouldEmitMethods(BinderMethodSpecifier.Instantiate)) + if (!_generationSpec.ShouldEmitMethods(BinderMethodSpecifier.Initialize)) { return; } - foreach (ObjectSpec type in _generationSpec.RootConfigTypes[BinderMethodSpecifier.Instantiate]) + foreach (ObjectSpec type in _generationSpec.ConfigTypes[BinderMethodSpecifier.Initialize]) { EmitBlankLineIfRequired(); - EmitInstantiateMethod(type); + EmitInitializeMethod(type); } } - private void EmitInstantiateMethod(ObjectSpec type) + private void EmitInitializeMethod(ObjectSpec type) { - Debug.Assert(type.ConstructorParameters.Count > 0); + Debug.Assert(type.CanInitialize); + List ctorParams = type.ConstructorParameters; + IEnumerable initOnlyProps = type.Properties.Values.Where(prop => prop.SetOnInit); string displayString = type.MinimalDisplayString; - _writer.WriteBlockStart($"public static {displayString} {type.InstantiateMethodName}({Identifier.IConfiguration} {Identifier.configuration}, {Identifier.BinderOptions}? {Identifier.binderOptions})"); + _writer.WriteBlockStart($"public static {displayString} {type.InitializeMethodDisplayString}({Identifier.IConfiguration} {Identifier.configuration}, {Identifier.BinderOptions}? {Identifier.binderOptions})"); - foreach (ParameterSpec parameter in type.ConstructorParameters) + foreach (ParameterSpec parameter in ctorParams) { - _writer.WriteLine($@"{parameter.Type.MinimalDisplayString} {parameter.Name} = {parameter.DefaultValue};"); + if (!parameter.HasExplicitDefaultValue) + { + _writer.WriteLine($@"({parameter.Type.MinimalDisplayString} {Identifier.Value}, bool {Identifier.HasConfig}) {parameter.Name} = ({parameter.DefaultValue}, false);"); + } + else + { + _writer.WriteLine($@"{parameter.Type.MinimalDisplayString} {parameter.Name} = {parameter.DefaultValue};"); + } } - foreach (PropertySpec property in type.InitOnlyProperties) + foreach (PropertySpec property in initOnlyProps) { - _writer.WriteLine($@"{property.Type.MinimalDisplayString} {property.Name} = default!;"); + if (property.MatchingCtorParam is null) + { + _writer.WriteLine($@"{property.Type.MinimalDisplayString} {property.Name} = default!;"); + } } _writer.WriteBlankLine(); @@ -372,19 +410,15 @@ private void EmitInstantiateMethod(ObjectSpec type) List argumentList = new(); - foreach (ParameterSpec parameter in type.ConstructorParameters) + foreach (ParameterSpec parameter in ctorParams) { - TypeSpec parameterType = parameter.Type; - string parameterName = parameter.Name; - string configurationKeyName = parameter.MatchingProperty?.ConfigurationKeyName ?? parameterName; - - EmitMemberBindLogic(parameterName, parameterType, configurationKeyName); - argumentList.Add(parameter.GetExpressionForArgument(parameterName)); + EmitMemberBindLogic(parameter.Name, parameter.Type, parameter.ConfigurationKeyName, configValueMustExist: !parameter.HasExplicitDefaultValue); + argumentList.Add(GetExpressionForArgument(parameter)); } - foreach (PropertySpec property in type.InitOnlyProperties) + foreach (PropertySpec property in initOnlyProps) { - if (property.ShouldBind()) + if (property.ShouldBind() && property.MatchingCtorParam is null) { EmitMemberBindLogic(property.Name, property.Type, property.ConfigurationKeyName); } @@ -399,40 +433,77 @@ private void EmitInstantiateMethod(ObjectSpec type) } """); - _writer.WriteBlankLine(); - _writer.WriteBlockStart($"return new {displayString}({string.Join(", ", argumentList)})"); - foreach (PropertySpec property in type.InitOnlyProperties) + _precedingBlockExists = true; + + foreach (ParameterSpec parameter in ctorParams) + { + if (!parameter.HasExplicitDefaultValue) + { + string parameterName = parameter.Name; + + EmitBlankLineIfRequired(); + _writer.WriteBlock($$""" + if (!{{parameterName}}.{{Identifier.HasConfig}}) + { + throw new {{GetInvalidOperationDisplayName()}}("{{string.Format(ExceptionMessages.ParameterHasNoMatchingConfig, type.Name, parameterName)}}"); + } + """); + } + } + + EmitBlankLineIfRequired(); + + string returnExpression = $"return new {displayString}({string.Join(", ", argumentList)})"; + if (!initOnlyProps.Any()) { - _writer.WriteLine($@"{property.Name} = {property.Name},"); + _writer.WriteLine($"{returnExpression};"); + } + else + { + _writer.WriteBlockStart(returnExpression); + foreach (PropertySpec property in initOnlyProps) + { + string propertyName = property.Name; + string initValue = propertyName + (property.MatchingCtorParam is null or ParameterSpec { HasExplicitDefaultValue: true } ? string.Empty : $".{Identifier.Value}"); + _writer.WriteLine($@"{propertyName} = {initValue},"); + } + _writer.WriteBlockEnd(";"); } - _writer.WriteBlockEnd(";"); // End method. _writer.WriteBlockEnd(); #region Local helpers - void EmitMemberBindLogic(string memberName, TypeSpec memberType, string configurationKeyName) + void EmitMemberBindLogic(string memberName, TypeSpec memberType, string configurationKeyName, bool configValueMustExist = false) { + string lhs = memberName + (configValueMustExist ? $".{Identifier.Value}" : string.Empty); + _writer.WriteBlockStart($@"case ""{configurationKeyName}"":"); - EmitMemberBindLogicCore(memberType, varName: memberName); + EmitMemberBindLogicCore(memberType, lhs); + + if (configValueMustExist) + { + _writer.WriteLine($"{memberName}.{Identifier.HasConfig} = true;"); + } + _writer.WriteBlockEnd(); _writer.WriteLine("break;"); - void EmitMemberBindLogicCore(TypeSpec type, string varName) + void EmitMemberBindLogicCore(TypeSpec type, string lhs) { TypeSpecKind kind = type.SpecKind; if (kind is TypeSpecKind.Nullable) { - EmitMemberBindLogicCore(((NullableSpec)type).UnderlyingType, varName); + EmitMemberBindLogicCore(((NullableSpec)type).UnderlyingType, lhs); } else if (kind is TypeSpecKind.ParsableFromString) { - EmitBindLogicFromString((ParsableFromStringSpec)type, varName, Expression.sectionValue, Expression.sectionPath); + EmitBindLogicFromString((ParsableFromStringSpec)type, lhs, Expression.sectionValue, Expression.sectionPath); } - else + else if (!EmitInitException(type)) { - EmitBindCoreCall(type, varName, Identifier.section, InitializationKind.SimpleAssignment); + EmitBindCoreCall(type, lhs, Identifier.section, InitializationKind.SimpleAssignment); } } } @@ -504,15 +575,15 @@ private void EmitHasChildrenMethod() private void EmitGetBinderOptionsHelper() { _writer.WriteBlock($$""" - public static {{Identifier.BinderOptions}}? {{Identifier.GetBinderOptions}}(System.Action? {{Identifier.configureActions}}) + public static {{Identifier.BinderOptions}}? {{Identifier.GetBinderOptions}}(System.Action? {{Identifier.configureOptions}}) { - if ({{Identifier.configureActions}} is null) + if ({{Identifier.configureOptions}} is null) { return null; } {{Identifier.BinderOptions}} {{Identifier.binderOptions}} = new(); - {{Identifier.configureActions}}({{Identifier.binderOptions}}); + {{Identifier.configureOptions}}({{Identifier.binderOptions}}); if ({{Identifier.binderOptions}}.BindNonPublicProperties) { @@ -598,8 +669,6 @@ private void EmitPrimitiveParseMethod(ParsableFromStringSpec type) } } - string exceptionTypeDisplayString = _useFullyQualifiedNames ? FullyQualifiedDisplayName.InvalidOperationException : Identifier.InvalidOperationException; - _writer.WriteBlock($$""" public static {{typeDisplayString}} {{type.ParseMethodName}}(string {{Identifier.stringValue}}, Func {{Identifier.getPath}}) { @@ -614,7 +683,7 @@ private void EmitPrimitiveParseMethod(ParsableFromStringSpec type) } catch ({{innerExceptionTypeDisplayString}} {{Identifier.exception}}) { - throw new {{exceptionTypeDisplayString}}($"{{exceptionArg1}}", {{Identifier.exception}}); + throw new {{GetInvalidOperationDisplayName()}}($"{{exceptionArg1}}", {{Identifier.exception}}); } } """); @@ -627,13 +696,17 @@ private void EmitBindCoreImpl(TypeSpec type) switch (type.SpecKind) { case TypeSpecKind.Enumerable: + case TypeSpecKind.Dictionary: + case TypeSpecKind.Object: { - EmitBindCoreImplForEnumerable((EnumerableSpec)type); + Debug.Assert(type.CanInitialize); + EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType); + EmitBindCoreImplForComplexType(type); } break; - case TypeSpecKind.Dictionary: + case TypeSpecKind.Nullable: { - EmitBindCoreImplForDictionary((DictionarySpec)type); + EmitBindCoreImpl(((NullableSpec)type).UnderlyingType); } break; case TypeSpecKind.IConfigurationSection: @@ -642,33 +715,29 @@ private void EmitBindCoreImpl(TypeSpec type) _writer.WriteLine($"{Identifier.obj} = {Identifier.section};"); } break; - case TypeSpecKind.Object: - { - EmitBindCoreImplForObject((ObjectSpec)type); - } - break; - case TypeSpecKind.Nullable: - { - EmitBindCoreImpl(((NullableSpec)type).UnderlyingType); - } - break; default: Debug.Fail("Invalid type kind", type.SpecKind.ToString()); break; } } - private void EmitBindCoreImplForEnumerable(EnumerableSpec type) + private void EmitBindCoreImplForComplexType(TypeSpec type) { - EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType); - - if (type.PopulationStrategy is CollectionPopulationStrategy.Array) + if (type.InitializationStrategy is InitializationStrategy.Array) { - EmitPopulationImplForArray(type); + EmitPopulationImplForArray((EnumerableSpec)type); + } + else if (type is EnumerableSpec enumerable) + { + EmitPopulationImplForEnumerableWithAdd(enumerable); + } + else if (type is DictionarySpec dictionary) + { + EmitBindCoreImplForDictionary(dictionary); } else { - EmitPopulationImplForEnumerableWithAdd(type); + EmitBindCoreImplForObject((ObjectSpec)type); } } @@ -725,14 +794,13 @@ private void EmitPopulationImplForEnumerableWithAdd(EnumerableSpec type) private void EmitBindCoreImplForDictionary(DictionarySpec type) { - EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType); + TypeSpec elementType = type.ElementType; EmitCollectionCastIfRequired(type, out string objIdentifier); _writer.WriteBlockStart($"foreach ({Identifier.IConfigurationSection} {Identifier.section} in {Identifier.configuration}.{Identifier.GetChildren}())"); ParsableFromStringSpec keyType = type.KeyType; - TypeSpec elementType = type.ElementType; // Parse key if (keyType.StringParsableTypeKind is StringParsableTypeKind.ConfigValue) @@ -776,6 +844,8 @@ void Emit_BindAndAddLogic_ForElement() } else // For complex types: { + Debug.Assert(elementType.CanInitialize); + bool isValueType = elementType.IsValueType; string expressionForElementIsNotNull = $"{Identifier.element} is not null"; string elementTypeDisplayString = elementType.MinimalDisplayString + (elementType.IsValueType ? string.Empty : "?"); @@ -795,13 +865,13 @@ void Emit_BindAndAddLogic_ForElement() if (elementType is CollectionSpec { - ConstructionStrategy: ConstructionStrategy.ParameterizedConstructor or ConstructionStrategy.ToEnumerableMethod + InitializationStrategy: InitializationStrategy.ParameterizedConstructor or InitializationStrategy.ToEnumerableMethod } collectionSpec) { // This is a read-only collection. If the element exists and is not null, // we need to copy its contents into a new instance & then append/bind to that. - string initExpression = collectionSpec.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor + string initExpression = collectionSpec.InitializationStrategy is InitializationStrategy.ParameterizedConstructor ? $"new {collectionSpec.ConcreteType.MinimalDisplayString}({Identifier.element})" : $"{Identifier.element}.{collectionSpec.ToEnumerableMethodCall!}"; @@ -823,31 +893,32 @@ void Emit_BindAndAddLogic_ForElement() private void EmitBindCoreImplForObject(ObjectSpec type) { - Dictionary properties = type.PropertiesBindableAfterInit; - if (properties.Count == 0) + if (type.Properties.Count == 0) { return; } - EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType); - string listOfStringDisplayName = "List"; _writer.WriteLine($"{listOfStringDisplayName}? {Identifier.temp} = null;"); _writer.WriteBlockStart($"foreach ({Identifier.IConfigurationSection} {Identifier.section} in {Identifier.configuration}.{Identifier.GetChildren}())"); _writer.WriteBlockStart($"switch ({Expression.sectionKey})"); - foreach (PropertySpec property in properties.Values) + foreach (PropertySpec property in type.Properties.Values) { _writer.WriteBlockStart($@"case ""{property.ConfigurationKeyName}"":"); + bool success = true; if (property.ShouldBind()) { - EmitBindCoreImplForProperty(property, property.Type!, parentType: type); + success = EmitBindCoreImplForProperty(property, property.Type, parentType: type); } _writer.WriteBlockEnd(); - _writer.WriteLine("break;"); + if (success) + { + _writer.WriteLine("break;"); + } } _writer.WriteBlock($$""" @@ -879,7 +950,7 @@ private void EmitBindCoreImplForObject(ObjectSpec type) } - private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propertyType, TypeSpec parentType) + private bool EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propertyType, TypeSpec parentType) { string configurationKeyName = property.ConfigurationKeyName; @@ -904,14 +975,6 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert } } break; - case TypeSpecKind.Array: - { - EmitBindCoreCallForProperty( - property, - propertyType, - expressionForPropertyAccess); - } - break; case TypeSpecKind.IConfigurationSection: { _writer.WriteLine($"{expressionForPropertyAccess} = {Identifier.section};"); @@ -925,10 +988,17 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert break; default: { + if (EmitInitException(propertyType)) + { + return false; + } + EmitBindCoreCallForProperty(property, propertyType, expressionForPropertyAccess); } break; } + + return true; } private void EmitBindLogicFromRootMethod(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind) @@ -939,22 +1009,25 @@ private void EmitBindLogicFromRootMethod(TypeSpec type, string expressionForMemb { EmitBindLogicFromRootMethod(((NullableSpec)type).UnderlyingType, expressionForMemberAccess, initKind); } - else if (kind is TypeSpecKind.ParsableFromString) + else { - if (initKind is InitializationKind.Declaration) + if (kind is TypeSpecKind.ParsableFromString) { - EmitCastToIConfigurationSection(); - _writer.WriteLine($"{GetTypeDisplayString(type)} {expressionForMemberAccess} = default!;"); + if (initKind is InitializationKind.Declaration) + { + EmitCastToIConfigurationSection(); + _writer.WriteLine($"{GetTypeDisplayString(type)} {expressionForMemberAccess} = default!;"); + } + else + { + EmitCastToIConfigurationSection(); + } + EmitBindLogicFromString((ParsableFromStringSpec)type, expressionForMemberAccess, Expression.sectionValue, Expression.sectionPath); } else { - EmitCastToIConfigurationSection(); + EmitBindCoreCall(type, expressionForMemberAccess, Identifier.configuration, initKind); } - EmitBindLogicFromString((ParsableFromStringSpec)type, expressionForMemberAccess, Expression.sectionValue, Expression.sectionPath); - } - else - { - EmitBindCoreCall(type, expressionForMemberAccess, Identifier.configuration, initKind); } } @@ -964,6 +1037,8 @@ private void EmitBindCoreCall( string expressionForConfigArg, InitializationKind initKind) { + Debug.Assert(type.CanInitialize); + string tempVarName = GetIncrementalVarName(Identifier.temp); if (initKind is InitializationKind.AssignmentWithNullCheck) { @@ -1032,24 +1107,27 @@ private void EmitBindCoreCallForProperty( _writer.WriteLine($"{expressionForPropertyAccess} = {tempVarName};"); } } - else if (canGet) + else { - _writer.WriteLine($"{effectivePropertyTypeDisplayString} {tempVarName} = {expressionForPropertyAccess};"); - EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.AssignmentWithNullCheck); - _writer.WriteLine($@"{Identifier.BindCore}({Identifier.section}, ref {tempVarName}, {Identifier.binderOptions});"); + if (canGet) + { + _writer.WriteLine($"{effectivePropertyTypeDisplayString} {tempVarName} = {expressionForPropertyAccess};"); + EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.AssignmentWithNullCheck); + _writer.WriteLine($@"{Identifier.BindCore}({Identifier.section}, ref {tempVarName}, {Identifier.binderOptions});"); - if (canSet) + if (canSet) + { + _writer.WriteLine($"{expressionForPropertyAccess} = {tempVarName};"); + } + } + else { + Debug.Assert(canSet); + EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.Declaration); + _writer.WriteLine($@"{Identifier.BindCore}({Identifier.section}, ref {tempVarName}, {Identifier.binderOptions});"); _writer.WriteLine($"{expressionForPropertyAccess} = {tempVarName};"); } } - else - { - Debug.Assert(canSet); - EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.Declaration); - _writer.WriteLine($@"{Identifier.BindCore}({Identifier.section}, ref {tempVarName}, {Identifier.binderOptions});"); - _writer.WriteLine($"{expressionForPropertyAccess} = {tempVarName};"); - } _writer.WriteBlockEnd(); } @@ -1079,11 +1157,14 @@ private void EmitBindLogicFromString( return; } - private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind) + private bool EmitObjectInit(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind) { + Debug.Assert(type.CanInitialize); + if (initKind is InitializationKind.None) { - return; + // Reachable? + return true; } string expressionForInit; @@ -1092,7 +1173,7 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini string effectiveDisplayString = GetTypeDisplayString(type); if (collectionType is not null) { - if (collectionType is EnumerableSpec { PopulationStrategy: CollectionPopulationStrategy.Array }) + if (collectionType is EnumerableSpec { InitializationStrategy: InitializationStrategy.Array }) { expressionForInit = $"new {_arrayBracketsRegex.Replace(effectiveDisplayString, "[0]", 1)}"; } @@ -1102,19 +1183,16 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini expressionForInit = $"new {effectiveDisplayString}()"; } } - else if (type.ConstructionStrategy is ConstructionStrategy.ParameterlessConstructor) + else if (type.InitializationStrategy is InitializationStrategy.ParameterlessConstructor) { expressionForInit = $"new {effectiveDisplayString}()"; } - else if (type.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor) - { - string expressionForConfigSection = initKind is InitializationKind.Declaration ? Identifier.configuration : Identifier.section; - string identifierForInstantiateMethod = GetHelperMethodDisplayString(((ObjectSpec)type).InstantiateMethodName); - expressionForInit = $"{identifierForInstantiateMethod}({expressionForConfigSection}, {Identifier.binderOptions});"; - } else { - return; + Debug.Assert(type.InitializationStrategy is InitializationStrategy.ParameterizedConstructor); + string expressionForConfigSection = initKind is InitializationKind.Declaration ? Identifier.configuration : Identifier.section; + string initMethodIdentifier = GetHelperMethodDisplayString(((ObjectSpec)type).InitializeMethodDisplayString); + expressionForInit = $"{initMethodIdentifier}({expressionForConfigSection}, {Identifier.binderOptions});"; } if (initKind == InitializationKind.Declaration) @@ -1126,10 +1204,10 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini { if (collectionType is CollectionSpec { - ConstructionStrategy: ConstructionStrategy.ParameterizedConstructor or ConstructionStrategy.ToEnumerableMethod + InitializationStrategy: InitializationStrategy.ParameterizedConstructor or InitializationStrategy.ToEnumerableMethod }) { - if (collectionType.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor) + if (collectionType.InitializationStrategy is InitializationStrategy.ParameterizedConstructor) { _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : new {effectiveDisplayString}({expressionForMemberAccess});"); } @@ -1148,6 +1226,8 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini Debug.Assert(initKind is InitializationKind.SimpleAssignment); _writer.WriteLine($"{expressionForMemberAccess} = {expressionForInit};"); } + + return true; } private void EmitCollectionCastIfRequired(CollectionSpec type, out string objIdentifier) 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 6511f66d669a2f..d705cc0ad59d19 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs @@ -19,7 +19,7 @@ private sealed class Parser private readonly SourceProductionContext _context; private readonly KnownTypeSymbols _typeSymbols; - private readonly Dictionary> _rootConfigTypes = new(); + private readonly Dictionary> _configTypes = new(); private readonly HashSet _unsupportedTypes = new(SymbolEqualityComparer.Default); private readonly Dictionary _createdSpecs = new(SymbolEqualityComparer.Default); @@ -84,7 +84,7 @@ public Parser(SourceProductionContext context, KnownTypeSymbols typeSymbols) } return new SourceGenerationSpec( - _rootConfigTypes, + _configTypes, _methodsToGen, _primitivesForHelperGen, _typeNamespaces.ToImmutableSortedSet()); @@ -319,21 +319,18 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) } } - private TypeSpec? AddRootConfigType(BinderMethodSpecifier methodGroup, BinderMethodSpecifier overload, ITypeSymbol type, Location? location) + private void AddRootConfigType(BinderMethodSpecifier methodGroup, BinderMethodSpecifier overload, ITypeSymbol type, Location? location) { if (type is INamedTypeSymbol namedType && ContainsGenericParameters(namedType)) { - return null; + return; } - TypeSpec? spec = GetOrCreateTypeSpec(type, location); - if (spec != null) + if (GetOrCreateTypeSpec(type, location) is TypeSpec spec) { - AddToRootConfigTypeCache(overload, spec); - AddToRootConfigTypeCache(methodGroup, spec, registerAsMethodToGen: false); + RegisterConfigType(spec, overload); + RegisterConfigType(spec, methodGroup, isMethodGroup: true); } - - return spec; } private TypeSpec? GetOrCreateTypeSpec(ITypeSymbol type, Location? location = null) @@ -343,6 +340,8 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) return spec; } + bool canInitialize = true; + if (IsNullable(type, out ITypeSymbol? underlyingType)) { spec = TryGetTypeSpec(underlyingType, ParserDiagnostics.NullableUnderlyingTypeNotSupported, out TypeSpec? underlyingTypeSpec) @@ -396,32 +395,36 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) _typeNamespaces.Add(@namespace); } - _createdSpecs[type] = spec; - return spec; + spec = canInitialize ? spec : null; + return _createdSpecs[type] = spec; void RegisterBindCoreGenType(TypeSpec? spec) { if (spec is not null) { - AddToRootConfigTypeCache(BinderMethodSpecifier.BindCore, spec); + if (spec.CanInitialize) + { + canInitialize = true; + RegisterConfigType(spec, BinderMethodSpecifier.BindCore); + } } } } - private void AddToRootConfigTypeCache(BinderMethodSpecifier method, TypeSpec spec, bool registerAsMethodToGen = true) + private void RegisterConfigType(TypeSpec spec, BinderMethodSpecifier binderMethod, bool isMethodGroup = false) { Debug.Assert(spec is not null); - if (!_rootConfigTypes.TryGetValue(method, out HashSet types)) + if (!_configTypes.TryGetValue(binderMethod, out HashSet types)) { - _rootConfigTypes[method] = types = new HashSet(); + _configTypes[binderMethod] = types = new HashSet(); } types.Add(spec); - if (registerAsMethodToGen) + if (!isMethodGroup) { - _methodsToGen |= method; + _methodsToGen |= binderMethod; } } @@ -563,23 +566,22 @@ private bool TryGetTypeSpec(ITypeSymbol type, DiagnosticDescriptor descriptor, o return null; } - // We want a BindCore method for List as a temp holder for the array values. - EnumerableSpec? listSpec = GetOrCreateTypeSpec(_typeSymbols.List.Construct(arrayType.ElementType)) as EnumerableSpec; - // We know the element type is supported. - Debug.Assert(listSpec != null); - if (listSpec is not null) - { - AddToRootConfigTypeCache(BinderMethodSpecifier.BindCore, listSpec); - } + // We want a BindCore method for List as a temp holder for the array values. We know the element type is supported. + EnumerableSpec listSpec = (GetOrCreateTypeSpec(_typeSymbols.List.Construct(arrayType.ElementType)) as EnumerableSpec)!; + RegisterConfigType(listSpec, BinderMethodSpecifier.BindCore); - return new EnumerableSpec(arrayType) + EnumerableSpec spec = new EnumerableSpec(arrayType) { Location = location, ElementType = elementSpec, ConcreteType = listSpec, - PopulationStrategy = CollectionPopulationStrategy.Array, + InitializationStrategy = InitializationStrategy.Array, + PopulationStrategy = CollectionPopulationStrategy.Cast_Then_Add, // Using the concrete list type as a temp holder. ToEnumerableMethodCall = null, }; + + Debug.Assert(spec.CanInitialize); + return spec; } private bool IsSupportedArrayType(ITypeSymbol type, Location? location) @@ -600,12 +602,23 @@ private bool IsSupportedArrayType(ITypeSymbol type, Location? location) private CollectionSpec? CreateCollectionSpec(INamedTypeSymbol type, Location? location) { + CollectionSpec? spec; if (IsCandidateDictionary(type, out ITypeSymbol keyType, out ITypeSymbol elementType)) { - return CreateDictionarySpec(type, location, keyType, elementType); + spec = CreateDictionarySpec(type, location, keyType, elementType); + Debug.Assert(spec is null or DictionarySpec { KeyType: null or ParsableFromStringSpec }); + } + else + { + spec = CreateEnumerableSpec(type, location); } - return CreateEnumerableSpec(type, location); + if (spec is not null) + { + spec.InitExceptionMessage ??= spec.ElementType.InitExceptionMessage; + } + + return spec; } private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? location, ITypeSymbol keyType, ITypeSymbol elementType) @@ -622,7 +635,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc return null; } - ConstructionStrategy constructionStrategy; + InitializationStrategy constructionStrategy; CollectionPopulationStrategy populationStrategy; INamedTypeSymbol? concreteType = null; INamedTypeSymbol? populationCastType = null; @@ -630,7 +643,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc if (HasPublicParameterLessCtor(type)) { - constructionStrategy = ConstructionStrategy.ParameterlessConstructor; + constructionStrategy = InitializationStrategy.ParameterlessConstructor; if (HasAddMethod(type, keyType, elementType)) { @@ -650,14 +663,14 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc else if (IsInterfaceMatch(type, _typeSymbols.GenericIDictionary_Unbound) || IsInterfaceMatch(type, _typeSymbols.IDictionary)) { concreteType = _typeSymbols.Dictionary; - constructionStrategy = ConstructionStrategy.ParameterlessConstructor; + constructionStrategy = InitializationStrategy.ParameterlessConstructor; populationStrategy = CollectionPopulationStrategy.Add; } else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyDictionary_Unbound)) { concreteType = _typeSymbols.Dictionary; populationCastType = _typeSymbols.GenericIDictionary; - constructionStrategy = ConstructionStrategy.ToEnumerableMethod; + constructionStrategy = InitializationStrategy.ToEnumerableMethod; populationStrategy = CollectionPopulationStrategy.Cast_Then_Add; toEnumerableMethodCall = "ToDictionary(pair => pair.Key, pair => pair.Value)"; _typeNamespaces.Add("System.Linq"); @@ -673,14 +686,14 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc Location = location, KeyType = (ParsableFromStringSpec)keySpec, ElementType = elementSpec, - ConstructionStrategy = constructionStrategy, + InitializationStrategy = constructionStrategy, PopulationStrategy = populationStrategy, ToEnumerableMethodCall = toEnumerableMethodCall, }; Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null)); - spec.ConcreteType = ConstructGenericCollectionTypeSpec(concreteType, keyType, elementType); - spec.PopulationCastType = ConstructGenericCollectionTypeSpec(populationCastType, keyType, elementType); + spec.ConcreteType = ConstructGenericCollectionSpecIfRequired(concreteType, keyType, elementType); + spec.PopulationCastType = ConstructGenericCollectionSpecIfRequired(populationCastType, keyType, elementType); return spec; } @@ -693,14 +706,14 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc return null; } - ConstructionStrategy constructionStrategy; + InitializationStrategy constructionStrategy; CollectionPopulationStrategy populationStrategy; INamedTypeSymbol? concreteType = null; INamedTypeSymbol? populationCastType = null; if (HasPublicParameterLessCtor(type)) { - constructionStrategy = ConstructionStrategy.ParameterlessConstructor; + constructionStrategy = InitializationStrategy.ParameterlessConstructor; if (HasAddMethod(type, elementType)) { @@ -721,34 +734,34 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc IsInterfaceMatch(type, _typeSymbols.GenericIList_Unbound)) { concreteType = _typeSymbols.List; - constructionStrategy = ConstructionStrategy.ParameterlessConstructor; + constructionStrategy = InitializationStrategy.ParameterlessConstructor; populationStrategy = CollectionPopulationStrategy.Add; } else if (IsInterfaceMatch(type, _typeSymbols.GenericIEnumerable_Unbound)) { concreteType = _typeSymbols.List; populationCastType = _typeSymbols.GenericICollection; - constructionStrategy = ConstructionStrategy.ParameterizedConstructor; + constructionStrategy = InitializationStrategy.ParameterizedConstructor; populationStrategy = CollectionPopulationStrategy.Cast_Then_Add; } else if (IsInterfaceMatch(type, _typeSymbols.ISet_Unbound)) { concreteType = _typeSymbols.HashSet; - constructionStrategy = ConstructionStrategy.ParameterlessConstructor; + constructionStrategy = InitializationStrategy.ParameterlessConstructor; populationStrategy = CollectionPopulationStrategy.Add; } else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlySet_Unbound)) { concreteType = _typeSymbols.HashSet; populationCastType = _typeSymbols.ISet; - constructionStrategy = ConstructionStrategy.ParameterizedConstructor; + constructionStrategy = InitializationStrategy.ParameterizedConstructor; populationStrategy = CollectionPopulationStrategy.Cast_Then_Add; } else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyList_Unbound) || IsInterfaceMatch(type, _typeSymbols.IReadOnlyCollection_Unbound)) { concreteType = _typeSymbols.List; populationCastType = _typeSymbols.GenericICollection; - constructionStrategy = ConstructionStrategy.ParameterizedConstructor; + constructionStrategy = InitializationStrategy.ParameterizedConstructor; populationStrategy = CollectionPopulationStrategy.Cast_Then_Add; } else @@ -763,113 +776,171 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc { Location = location, ElementType = elementSpec, - ConstructionStrategy = constructionStrategy, + InitializationStrategy = constructionStrategy, PopulationStrategy = populationStrategy, ToEnumerableMethodCall = null, }; Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null)); - spec.ConcreteType = ConstructGenericCollectionTypeSpec(concreteType, elementType); - spec.PopulationCastType = ConstructGenericCollectionTypeSpec(populationCastType, elementType); + spec.ConcreteType = ConstructGenericCollectionSpecIfRequired(concreteType, elementType); + spec.PopulationCastType = ConstructGenericCollectionSpecIfRequired(populationCastType, elementType); return spec; } private ObjectSpec? CreateObjectSpec(INamedTypeSymbol type, Location? location) { - Debug.Assert(!_createdSpecs.ContainsKey(type)); - - IMethodSymbol? ctor = GetConstructor(type); - // Add spec to cache before traversing properties to avoid stack overflow. + ObjectSpec objectSpec = new(type) { Location = location }; + _createdSpecs.Add(type, objectSpec); - if (ctor is null) - { - ReportUnsupportedType(type, ParserDiagnostics.NeedPublicParameterlessConstructor, location); - _createdSpecs.Add(type, null); - return null; - } - - ConstructionStrategy constructionStrategy = ctor.Parameters.Length is 0 ? ConstructionStrategy.ParameterlessConstructor : ConstructionStrategy.ParameterizedConstructor; + string typeName = objectSpec.Name; + IMethodSymbol? ctor = null; + DiagnosticDescriptor? diagnosticDescriptor = null; - ObjectSpec objectSpec = new(type) + if (!(type.IsAbstract || type.TypeKind is TypeKind.Interface)) { - Location = location, - ConstructionStrategy = constructionStrategy, - }; + IMethodSymbol? parameterlessCtor = null; + IMethodSymbol? parameterizedCtor = null; + bool hasMultipleParameterizedCtors = false; - _createdSpecs.Add(type, objectSpec); + foreach (IMethodSymbol candidate in type.InstanceConstructors) + { + if (candidate.DeclaredAccessibility is not Accessibility.Public) + { + continue; + } - Dictionary? parameterCache = null; - if (constructionStrategy is ConstructionStrategy.ParameterizedConstructor) - { - AddToRootConfigTypeCache(BinderMethodSpecifier.Instantiate, objectSpec); + if (candidate.Parameters.Length is 0) + { + parameterlessCtor = candidate; + } + else if (parameterizedCtor is not null) + { + hasMultipleParameterizedCtors = true; + } + else + { + parameterizedCtor = candidate; + } + } - parameterCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (IParameterSymbol parameter in ctor.Parameters) + bool hasPublicParameterlessCtor = type.IsValueType || parameterlessCtor is not null; + if (!hasPublicParameterlessCtor && hasMultipleParameterizedCtors) { - ParameterSpec parameterSpec = new ParameterSpec(parameter) { Type = GetOrCreateTypeSpec(parameter.Type) }; - parameterCache[parameter.Name] = parameterSpec; - objectSpec.ConstructorParameters.Add(parameterSpec); + diagnosticDescriptor = ParserDiagnostics.MultipleParameterizedConstructors; + objectSpec.InitExceptionMessage = string.Format(ExceptionMessages.MultipleParameterizedConstructors, typeName); } + + ctor = parameterizedCtor ?? parameterlessCtor; + } + + objectSpec.InitializationStrategy = ctor?.Parameters.Length is 0 ? InitializationStrategy.ParameterlessConstructor : InitializationStrategy.ParameterizedConstructor; + + if (ctor is null) + { + diagnosticDescriptor = ParserDiagnostics.MissingPublicInstanceConstructor; + objectSpec.InitExceptionMessage = string.Format(ExceptionMessages.MissingPublicInstanceConstructor, typeName); + } + + if (diagnosticDescriptor is not null) + { + Debug.Assert(objectSpec.InitExceptionMessage is not null); + ReportUnsupportedType(type, diagnosticDescriptor); + return objectSpec; } INamedTypeSymbol current = type; while (current is not null) { - foreach (ISymbol member in current.GetMembers()) + var members = current.GetMembers(); + foreach (ISymbol member in members) { - if (member is IPropertySymbol { IsIndexer: false } property) + if (member is IPropertySymbol { IsIndexer: false, IsImplicitlyDeclared: false } property) { - if (property.Type is ITypeSymbol { } propertyType) - { - AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute)); - string propertyName = property.Name; - string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName; - - TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(propertyType); - if (propertyTypeSpec is null) - { - _context.ReportDiagnostic(Diagnostic.Create(ParserDiagnostics.PropertyNotSupported, location, new string[] { propertyName, type.ToDisplayString() })); - } - else - { - RegisterHasChildrenHelperForGenIfRequired(propertyTypeSpec); - } + AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute)); + string propertyName = property.Name; + string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName; + TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(property.Type); + if (propertyTypeSpec is null) + { + _context.ReportDiagnostic(Diagnostic.Create(ParserDiagnostics.PropertyNotSupported, location, new string[] { propertyName, type.ToDisplayString() })); + } + else + { PropertySpec spec = new(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; - - ParameterSpec? matchingParam = null; - if ((parameterCache?.TryGetValue(propertyName, out matchingParam)) is true && - matchingParam.Type == propertyTypeSpec) - { - matchingParam.MatchingProperty = spec; - } - - if (property.IsRequired || property.SetMethod?.IsInitOnly is true) - { - if (matchingParam is null) - { - objectSpec.InitOnlyProperties.Add(spec); - } - } - else - { - objectSpec.PropertiesBindableAfterInit[configKeyName] = spec; - } + objectSpec.Properties[propertyName] = spec; + RegisterHasChildrenHelperForGenIfRequired(propertyTypeSpec); } } } current = current.BaseType; } + if (objectSpec.InitializationStrategy is InitializationStrategy.ParameterizedConstructor) + { + List missingParameters = new(); + List invalidParameters = new(); + + foreach (IParameterSymbol parameter in ctor.Parameters) + { + string parameterName = parameter.Name; + + if (!objectSpec.Properties.TryGetValue(parameterName, out PropertySpec? propertySpec)) + { + missingParameters.Add(parameterName); + } + else if (parameter.RefKind is not RefKind.None) + { + invalidParameters.Add(parameterName); + } + else + { + ParameterSpec paramSpec = new ParameterSpec(parameter) + { + Type = propertySpec.Type, + ConfigurationKeyName = propertySpec.ConfigurationKeyName, + }; + + propertySpec.MatchingCtorParam = paramSpec; + objectSpec.ConstructorParameters.Add(paramSpec); + } + } + + if (invalidParameters.Count > 0) + { + objectSpec.InitExceptionMessage = string.Format(ExceptionMessages.CannotBindToConstructorParameter, typeName, FormatParams(invalidParameters)); + } + else if (missingParameters.Count > 0) + { + if (type.IsValueType) + { + objectSpec.InitializationStrategy = InitializationStrategy.ParameterlessConstructor; + } + else + { + objectSpec.InitExceptionMessage = string.Format(ExceptionMessages.ConstructorParametersDoNotMatchProperties, typeName, FormatParams(missingParameters)); + } + } + + if (objectSpec.CanInitialize) + { + RegisterConfigType(objectSpec, BinderMethodSpecifier.Initialize); + } + + static string FormatParams(List names) => string.Join(",", names); + } + + Debug.Assert((objectSpec.CanInitialize && objectSpec.InitExceptionMessage is null) || + (!objectSpec.CanInitialize && objectSpec.InitExceptionMessage is not null)); + return objectSpec; } private void RegisterHasChildrenHelperForGenIfRequired(TypeSpec type) { if (type.SpecKind is TypeSpecKind.Object or - TypeSpecKind.Array or TypeSpecKind.Enumerable or TypeSpecKind.Dictionary) { @@ -965,53 +1036,7 @@ public static bool ContainsGenericParameters(INamedTypeSymbol type) } private static bool HasPublicParameterLessCtor(INamedTypeSymbol type) => - CannotBeConstructed(type) - ? false - : type.InstanceConstructors.SingleOrDefault(ctor => ctor.DeclaredAccessibility is Accessibility.Public && ctor.Parameters.Length is 0) - is not null; - - private static IMethodSymbol? GetConstructor(INamedTypeSymbol type) - { - if (CannotBeConstructed(type)) - { - return null; - } - - IMethodSymbol? publicParameterlessCtor = null; - IMethodSymbol? publicParameterizedCtor = null; - bool hasMultiplePublicParameterizedCtors = false; - - foreach (IMethodSymbol ctor in type.InstanceConstructors) - { - if (ctor.DeclaredAccessibility is not Accessibility.Public) - { - continue; - } - - if (ctor.Parameters.Length is 0) - { - publicParameterlessCtor = ctor; - } - else - { - if (publicParameterizedCtor is not null) - { - hasMultiplePublicParameterizedCtors = true; - } - - publicParameterizedCtor = ctor; - } - } - - if (publicParameterlessCtor is null && hasMultiplePublicParameterizedCtors) - { - return null; - } - - return publicParameterizedCtor ?? publicParameterlessCtor; - } - - private static bool CannotBeConstructed(INamedTypeSymbol type) => type.IsAbstract || type.TypeKind == TypeKind.Interface; + type.InstanceConstructors.SingleOrDefault(ctor => ctor.DeclaredAccessibility is Accessibility.Public && ctor.Parameters.Length is 0) is not null; private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol element) { @@ -1048,7 +1073,7 @@ private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol key, ITypeSy private static bool IsEnum(ITypeSymbol type) => type is INamedTypeSymbol { EnumUnderlyingType: INamedTypeSymbol { } }; - private CollectionSpec? ConstructGenericCollectionTypeSpec(INamedTypeSymbol? collectionType, params ITypeSymbol[] parameters) => + private CollectionSpec? ConstructGenericCollectionSpecIfRequired(INamedTypeSymbol? collectionType, params ITypeSymbol[] parameters) => (collectionType is not null ? ConstructGenericCollectionSpec(collectionType, parameters) : null); private CollectionSpec? ConstructGenericCollectionSpec(INamedTypeSymbol type, params ITypeSymbol[] parameters) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter.Helpers.cs index 79290366e720d2..b0d3756b068682 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/Emitter.Helpers.cs @@ -1,6 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.CodeAnalysis; +using System; +using System.Diagnostics; + namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { public sealed partial class ConfigurationBindingGenerator @@ -39,7 +43,7 @@ private static class FullyQualifiedDisplayName public static class Identifier { public const string binderOptions = nameof(binderOptions); - public const string configureActions = nameof(configureActions); + public const string configureOptions = nameof(configureOptions); public const string configuration = nameof(configuration); public const string defaultValue = nameof(defaultValue); public const string element = nameof(element); @@ -79,6 +83,7 @@ public static class Identifier public const string GetValue = nameof(GetValue); public const string GetValueCore = nameof(GetValueCore); public const string HasChildren = nameof(HasChildren); + public const string HasConfig = nameof(HasConfig); public const string HasValueOrChildren = nameof(HasValueOrChildren); public const string HasValue = nameof(HasValue); public const string Helpers = nameof(Helpers); @@ -133,6 +138,21 @@ private void EmitCheckForNullArgument_WithBlankLine(string argName, bool useFull _writer.WriteBlankLine(); } + private bool EmitInitException(TypeSpec type) + { + Debug.Assert(type.InitializationStrategy is not InitializationStrategy.None); + + if (!type.CanInitialize) + { + _writer.WriteLine(GetInitException(type.InitExceptionMessage) + ";"); + return true; + } + + return false; + } + + private string GetInitException(string message) => $@"throw new {GetInvalidOperationDisplayName()}(""{message}"")"; + private string GetIncrementalVarName(string prefix) => $"{prefix}{_parseValueCount++}"; private string GetTypeDisplayString(TypeSpec type) => _useFullyQualifiedNames ? type.FullyQualifiedDisplayString : type.MinimalDisplayString; @@ -146,6 +166,22 @@ private string GetHelperMethodDisplayString(string methodName) return methodName; } + + private static string GetExpressionForArgument(ParameterSpec parameter) + { + string name = parameter.Name + (parameter.HasExplicitDefaultValue ? string.Empty : $".{Identifier.Value}"); + + return parameter.RefKind switch + { + RefKind.None => name, + RefKind.Ref => $"ref {name}", + RefKind.Out => "out _", + RefKind.In => $"in {name}", + _ => throw new InvalidOperationException() + }; + } + + private string GetInvalidOperationDisplayName() => _useFullyQualifiedNames ? FullyQualifiedDisplayName.InvalidOperationException : Identifier.InvalidOperationException; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/ExceptionMessages.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/ExceptionMessages.cs index a90f015ec8ea29..223c19c373c001 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/ExceptionMessages.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/ExceptionMessages.cs @@ -6,9 +6,15 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration // Runtime exception messages; not localized so we keep them in source. internal static class ExceptionMessages { + public const string CannotBindToConstructorParameter = "Cannot create instance of type '{0}' because one or more parameters cannot be bound to. Constructor parameters cannot be declared as in, out, or ref. Invalid parameters are: '{1}'"; public const string CannotSpecifyBindNonPublicProperties = "The configuration binding source generator does not support 'BinderOptions.BindNonPublicProperties'."; + public const string ConstructorParametersDoNotMatchProperties = "Cannot create instance of type '{0}' because one or more parameters cannot be bound to. Constructor parameters must have corresponding properties. Fields are not supported. Missing properties are: '{1}'"; public const string FailedBinding = "Failed to convert configuration value at '{0}' to type '{1}'."; public const string MissingConfig = "'{0}' was set on the provided {1}, but the following properties were not found on the instance of {2}: {3}"; + public const string MissingPublicInstanceConstructor = "Cannot create instance of type '{0}' because it is missing a public instance constructor."; + public const string MultipleParameterizedConstructors = "Cannot create instance of type '{0}' because it has multiple public parameterized constructors."; + public const string ParameterBeingBoundToIsUnnamed = "Cannot create instance of type '{0}' because one or more parameters are unnamed."; + public const string ParameterHasNoMatchingConfig = "Cannot create instance of type '{0}' because parameter '{1}' has no matching config. Each parameter in the constructor that does not have a default value must have a corresponding config entry."; public const string TypeNotDetectedAsInput = "Unable to bind to type '{0}': generator did not detect the type as input."; public const string TypeNotSupportedAsInput = "Unable to bind to type '{0}': generator does not support this type as input to this method."; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/ParserDiagnostics.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/ParserDiagnostics.cs index 279eea98189be1..be2631422fe391 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/ParserDiagnostics.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Helpers/ParserDiagnostics.cs @@ -9,10 +9,11 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration internal static class ParserDiagnostics { public static DiagnosticDescriptor TypeNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.TypeNotSupported)); - public static DiagnosticDescriptor NeedPublicParameterlessConstructor { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.NeedPublicParameterlessConstructor)); + public static DiagnosticDescriptor MissingPublicInstanceConstructor { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.MissingPublicInstanceConstructor)); public static DiagnosticDescriptor CollectionNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.CollectionNotSupported)); public static DiagnosticDescriptor DictionaryKeyNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.DictionaryKeyNotSupported)); public static DiagnosticDescriptor ElementTypeNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.ElementTypeNotSupported)); + public static DiagnosticDescriptor MultipleParameterizedConstructors { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.MultipleParameterizedConstructors)); public static DiagnosticDescriptor MultiDimArraysNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.MultiDimArraysNotSupported)); public static DiagnosticDescriptor NullableUnderlyingTypeNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.NullableUnderlyingTypeNotSupported)); 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 0a3786607c66d3..c3929ec6ba6662 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 @@ -33,7 +33,7 @@ - + diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/BinderMethodSpecifier.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/BinderMethodSpecifier.cs index a34b5818f34e18..33b2859a867bfa 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/BinderMethodSpecifier.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/BinderMethodSpecifier.cs @@ -74,7 +74,7 @@ internal enum BinderMethodSpecifier // Binding helpers BindCore = 0x1000, HasChildren = 0x4000, - Instantiate = 0x8000, + Initialize = 0x8000, // Method groups Bind = Bind_instance | Bind_instance_BinderOptions | Bind_key_instance, diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/CollectionSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/CollectionSpec.cs index 7054a3513edd97..7c90701fbda035 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/CollectionSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/CollectionSpec.cs @@ -17,6 +17,10 @@ public CollectionSpec(ITypeSymbol type) : base(type) { } public required CollectionPopulationStrategy PopulationStrategy { get; init; } + public override bool CanInitialize => ConcreteType?.CanInitialize ?? CanInitCompexType; + + public override required InitializationStrategy InitializationStrategy { get; set; } + public required string? ToEnumerableMethodCall { get; init; } } @@ -38,9 +42,8 @@ public DictionarySpec(INamedTypeSymbol type) : base(type) { } internal enum CollectionPopulationStrategy { - Unknown, - Array, - Add, - Cast_Then_Add, + Unknown = 0, + Add = 1, + Cast_Then_Add = 2, } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ConstructionStrategy.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/InitializationStrategy.cs similarity index 85% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ConstructionStrategy.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/InitializationStrategy.cs index d5c454bcecb5ec..866dd254e0181e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ConstructionStrategy.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/InitializationStrategy.cs @@ -3,11 +3,12 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { - internal enum ConstructionStrategy + internal enum InitializationStrategy { None = 0, ParameterlessConstructor = 1, ParameterizedConstructor = 2, ToEnumerableMethod = 3, + Array = 4, } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/NullableSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/NullableSpec.cs index e79696f661277d..c59e17e3c24240 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/NullableSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/NullableSpec.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 Microsoft.CodeAnalysis; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration @@ -12,5 +13,11 @@ public NullableSpec(ITypeSymbol type) : base(type) { } public override TypeSpecKind SpecKind => TypeSpecKind.Nullable; public required TypeSpec UnderlyingType { get; init; } + + public override string? InitExceptionMessage + { + get => UnderlyingType.InitExceptionMessage; + set => throw new InvalidOperationException(); + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs index f81da45f284e8a..4dbfc4a1df9bf7 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ObjectSpec.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.Generic; using Microsoft.CodeAnalysis; @@ -10,13 +11,19 @@ internal sealed record ObjectSpec : TypeSpec { public ObjectSpec(INamedTypeSymbol type) : base(type) { - InstantiateMethodName = $"Initialize{MinimalDisplayString.Replace(".", string.Empty).Replace("<", string.Empty).Replace(">", string.Empty)}"; + InitializeMethodDisplayString = $"Initialize{type.Name.Replace(".", string.Empty).Replace("<", string.Empty).Replace(">", string.Empty)}"; } public override TypeSpecKind SpecKind => TypeSpecKind.Object; + + public override InitializationStrategy InitializationStrategy { get; set; } + + public override bool CanInitialize => CanInitCompexType; + + public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); + public List ConstructorParameters { get; } = new(); - public Dictionary PropertiesBindableAfterInit { get; } = new(); - public List InitOnlyProperties { get; } = new(); - public string InstantiateMethodName { get; } + + public string? InitializeMethodDisplayString { get; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs index f4bd6c9d46fef6..a62f6080537ba2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs @@ -1,7 +1,6 @@ // 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 Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -9,31 +8,29 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { internal sealed record ParameterSpec { - public required TypeSpec? Type { get; init; } - public string Name { get; } - public PropertySpec? MatchingProperty { get; set; } - public RefKind RefKind { get; } - public string DefaultValue { get; } = "default!"; - public ParameterSpec(IParameterSymbol parameter) { Name = parameter.Name; RefKind = parameter.RefKind; - if (parameter.HasExplicitDefaultValue) + HasExplicitDefaultValue = parameter.HasExplicitDefaultValue; + if (HasExplicitDefaultValue) { string formatted = SymbolDisplay.FormatPrimitive(parameter.ExplicitDefaultValue, quoteStrings: true, useHexadecimalNumbers: false); DefaultValue = formatted is "null" ? "default!" : formatted; } } - public string GetExpressionForArgument(string argument) => RefKind switch - { - RefKind.None => argument, - RefKind.Ref => $"ref {argument}", - RefKind.Out => "out _", - RefKind.In => $"in {argument}", - _ => throw new InvalidOperationException("Unknown ref kind") - }; + public required TypeSpec Type { get; init; } + + public string Name { get; } + + public required string ConfigurationKeyName { get; init; } + + public RefKind RefKind { get; } + + public bool HasExplicitDefaultValue { get; init; } + + public string DefaultValue { get; } = "default!"; } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/PropertySpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/PropertySpec.cs index c307c9b2b3a362..35d79a296eda73 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/PropertySpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/PropertySpec.cs @@ -11,25 +11,37 @@ public PropertySpec(IPropertySymbol property) { Name = property.Name; IsStatic = property.IsStatic; - CanGet = property.GetMethod is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsInitOnly: false }; - CanSet = property.SetMethod is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsInitOnly: false }; + + bool setterIsPublic = property.SetMethod?.DeclaredAccessibility is Accessibility.Public; + IsInitOnly = property.SetMethod?.IsInitOnly == true; + IsRequired = property.IsRequired; + SetOnInit = setterIsPublic && (IsInitOnly || IsRequired); + CanSet = setterIsPublic && !IsInitOnly; + CanGet = property.GetMethod?.DeclaredAccessibility is Accessibility.Public; } + public required TypeSpec Type { get; init; } + + public ParameterSpec? MatchingCtorParam { get; set; } + public string Name { get; } public bool IsStatic { get; } + public bool IsRequired { get; } + + public bool IsInitOnly { get; } + + public bool SetOnInit { get; } + public bool CanGet { get; } public bool CanSet { get; } - public required TypeSpec? Type { get; init; } - public required string ConfigurationKeyName { get; init; } public bool ShouldBind() => (CanGet || CanSet) && - Type is not null && - !(!CanSet && (Type as CollectionSpec)?.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor); + !(!CanSet && (Type as CollectionSpec)?.InitializationStrategy is InitializationStrategy.ParameterizedConstructor); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs index db88141e551f33..f33a2980d4d5b4 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { internal sealed record SourceGenerationSpec( - Dictionary> RootConfigTypes, + Dictionary> ConfigTypes, BinderMethodSpecifier MethodsToGen, HashSet PrimitivesForHelperGen, ImmutableSortedSet TypeNamespaces) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs index b7a05abae21cc5..f53b92960230ac 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/TypeSpec.cs @@ -15,12 +15,15 @@ internal abstract record TypeSpec public TypeSpec(ITypeSymbol type) { + IsValueType = type.IsValueType; + Namespace = type.ContainingNamespace?.ToDisplayString(); FullyQualifiedDisplayString = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); MinimalDisplayString = type.ToDisplayString(s_minimalDisplayFormat); - Namespace = type.ContainingNamespace?.ToDisplayString(); - IsValueType = type.IsValueType; + Name = Namespace + "." + MinimalDisplayString.Replace(".", "+"); } + public string Name { get; } + public string FullyQualifiedDisplayString { get; } public string MinimalDisplayString { get; } @@ -31,12 +34,18 @@ public TypeSpec(ITypeSymbol type) public abstract TypeSpecKind SpecKind { get; } - public virtual ConstructionStrategy ConstructionStrategy { get; set; } + public virtual InitializationStrategy InitializationStrategy { get; set; } + + public virtual string? InitExceptionMessage { get; set; } + + public virtual bool CanInitialize => true; /// - /// Where in the input compilation we picked up a call to Bind, Get, or Configure. + /// Location in the input compilation we picked up a call to Bind, Get, or Configure. /// public required Location? Location { get; init; } + + protected bool CanInitCompexType => InitializationStrategy is not InitializationStrategy.None && InitExceptionMessage is null; } internal enum TypeSpecKind @@ -44,10 +53,9 @@ internal enum TypeSpecKind Unknown = 0, ParsableFromString = 1, Object = 2, - Array = 3, - Enumerable = 4, - Dictionary = 5, - IConfigurationSection = 6, - Nullable = 7, + Enumerable = 3, + Dictionary = 4, + IConfigurationSection = 5, + Nullable = 6, } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/Strings.resx index 690b71da3115a8..9bf38777bde79a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/Strings.resx @@ -132,11 +132,14 @@ Language version is required to be at least C# 11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Multidimensional arrays are not supported: '{0}'. - - Only objects with public parameterless constructors are supported: '{0}'. + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. Nullable underlying type is not supported: '{0}'. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.cs.xlf index 0c8b59da3a29fb..67cc2e5f23ea48 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.cs.xlf @@ -27,14 +27,19 @@ Verze jazyka musí být alespoň C# 11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. Multidimenzionální pole se nepodporují: {0}“. - - Only objects with public parameterless constructors are supported: '{0}'. - Podporují se pouze objekty s veřejnými konstruktory bez parametrů: „{0}“. + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.de.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.de.xlf index adfcd49151c285..9c6839a3fd2fac 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.de.xlf @@ -27,14 +27,19 @@ Die Sprachversion muss mindestens C# 11 sein + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. Mehrdimensionale Arrays werden nicht unterstützt: "{0}". - - Only objects with public parameterless constructors are supported: '{0}'. - Nur Objekte mit öffentlichen parameterlosen Konstruktoren werden unterstützt: "{0}". + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.es.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.es.xlf index a344ff879276f8..f876dcd5bd38fb 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.es.xlf @@ -27,14 +27,19 @@ La versión del lenguaje debe ser al menos C# 11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. No se admiten las matrices multidimensionales: "{0}". - - Only objects with public parameterless constructors are supported: '{0}'. - Solo se admiten objetos con constructores públicos sin parámetros: "{0}". + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.fr.xlf index 1ff77875802777..cab1b66b3079be 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.fr.xlf @@ -27,14 +27,19 @@ La version du langage doit être au moins C# 11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. Les tableaux multidimensionnels ne sont pas pris en charge : ‘{0}‘. - - Only objects with public parameterless constructors are supported: '{0}'. - Seuls les objets avec des constructeurs sans paramètre publics sont pris en charge : ‘{0}‘. + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.it.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.it.xlf index 957605d1359024..a212bd81626656 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.it.xlf @@ -27,14 +27,19 @@ La versione del linguaggio deve essere almeno C# 11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. Matrici multidimensionali non supportate: '{0}'. - - Only objects with public parameterless constructors are supported: '{0}'. - Sono supportati solo oggetti con costruttori senza parametri pubblici: '{0}'. + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ja.xlf index 76d85146fb356a..b6c9407b5d16dd 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ja.xlf @@ -27,14 +27,19 @@ 言語バージョンは少なくとも C# 11 である必要があります + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. 多次元配列はサポートされていません: '{0}'. - - Only objects with public parameterless constructors are supported: '{0}'. - パラメーターなしのパブリック コンストラクターを持つオブジェクトのみがサポートされています: '{0}'。 + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ko.xlf index bcf4ea9b33e8bc..0bd5f2f45bbdb6 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ko.xlf @@ -27,14 +27,19 @@ 언어 버전은 C# 11 이상이어야 합니다. + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. 다차원 배열은 지원되지 않습니다: '{0}'. - - Only objects with public parameterless constructors are supported: '{0}'. - 공용 매개 변수 없는 생성자가 있는 개체만 지원됩니다: '{0}'. + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pl.xlf index 70462e08f1fffe..aeb93e0bfb93ee 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pl.xlf @@ -27,14 +27,19 @@ Wymagana jest wersja językowa co najmniej C# 11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. Tablice wielowymiarowe nie są obsługiwane: „{0}”. - - Only objects with public parameterless constructors are supported: '{0}'. - Obsługiwane są tylko obiekty z publicznymi konstruktorami bez parametrów: „{0}”. + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pt-BR.xlf index 74a4a196c11c18..8e8254c995d9f6 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pt-BR.xlf @@ -27,14 +27,19 @@ A versão do idioma deve ser pelo menos C# 11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. Matrizes multidimensionais não são suportadas: '{0}'. - - Only objects with public parameterless constructors are supported: '{0}'. - Somente objetos com construtores públicos sem parâmetros são suportados: '{0}'. + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ru.xlf index 7ae5c07ecd580d..eba8abf1e9fd4e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ru.xlf @@ -27,14 +27,19 @@ Версия языка должна быть не ниже C# 11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. Многомерные массивы не поддерживаются: "{0}". - - Only objects with public parameterless constructors are supported: '{0}'. - Поддерживаются только объекты с общедоступными конструкторами без параметров: "{0}". + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.tr.xlf index 2d44681bded57d..ecb3ba09f40824 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.tr.xlf @@ -27,14 +27,19 @@ Dil sürümünün en az C# 11 olması gerekir + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. Çok boyutlu diziler desteklenmiyor: '{0}'. - - Only objects with public parameterless constructors are supported: '{0}'. - Yalnızca genel parametresiz oluşturucular içeren nesneler desteklenmiyor: '{0}'. + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hans.xlf index 15f0afb1e4916b..6ca3d43c80c6b9 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -27,14 +27,19 @@ 语言版本必须至少为 C# 11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. 不支持多维数组: '{0}'。 - - Only objects with public parameterless constructors are supported: '{0}'. - 仅支持具有公共无参数构造函数的对象: '{0}'。 + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hant.xlf index c487ecd57310ef..64cde87ba66ec9 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -27,14 +27,19 @@ 語言版本要求至少為 C#11 + + Cannot create instance of type '{0}' because it is missing a public instance constructor. + Cannot create instance of type '{0}' because it is missing a public instance constructor. + + Multidimensional arrays are not supported: '{0}'. 不支援多維陣列: '{0}'。 - - Only objects with public parameterless constructors are supported: '{0}'. - 僅支援具有公用無參數建構函式的物件: '{0}'。 + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. 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 56e03b5f768204..6e30c7400d78af 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 @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text.Json.Serialization; using Microsoft.Extensions.Configuration; using Xunit; @@ -114,6 +113,11 @@ public ClassWhereParametersHaveDefaultValue(string? name, string address, int ag } } + public class ClassWithPrimaryCtor(string color, int length) + { + public string Color { get; } = color; + public int Length { get; } = length; + } public record RecordTypeOptions(string Color, int Length); 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 afb6cb5de33987..ea3f402527b893 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -42,6 +42,8 @@ public void BindWithNestedTypesWithReadOnlyProperties() Assert.Equal("Dummy", result.Nested.MyProp); } + // Add test for type with parameterless ctor + init-only properties. + [Fact] public void EnumBindCaseInsensitiveNotThrows() { @@ -920,8 +922,8 @@ public void ExceptionWhenTryingToBindClassWithoutParameterlessConstructor() exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. - public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParameters() + [Fact] + public void ExceptionWhenTryingToBindClassWherePropertiesDoNotMatchConstructorParameters() { var input = new Dictionary { @@ -941,7 +943,7 @@ public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParam exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() { var input = new Dictionary @@ -961,7 +963,7 @@ public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor() { var input = new Dictionary @@ -1003,7 +1005,7 @@ public void BindsToClassConstructorParametersWithDefaultValues() Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.Age); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void FieldsNotSupported_ExceptionBindingToConstructorWithParameterMatchingAField() { var input = new Dictionary @@ -1025,7 +1027,7 @@ public void FieldsNotSupported_ExceptionBindingToConstructorWithParameterMatchin exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void BindsToRecordPrimaryConstructorParametersWithDefaultValues() { var input = new Dictionary @@ -1139,7 +1141,7 @@ public void CanBindMutableClassWitNestedImmutableObject() // If the immutable type has multiple public parameterized constructors, then throw // an exception. - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void CanBindImmutableClass_ThrowsOnMultipleParameterizedConstructors() { var dic = new Dictionary @@ -1153,7 +1155,7 @@ public void CanBindImmutableClass_ThrowsOnMultipleParameterizedConstructors() configurationBuilder.AddInMemoryCollection(dic); var config = configurationBuilder.Build(); - string expectedMessage = SR.Format(SR.Error_MultipleParameterizedConstructors, "Microsoft.Extensions.Configuration.Binder.Tests.ConfigurationBinderTests+ImmutableClassWithMultipleParameterizedConstructors"); + string expectedMessage = SR.Format(SR.Error_MultipleParameterizedConstructors, typeof(ImmutableClassWithMultipleParameterizedConstructors)); var ex = Assert.Throws(() => config.Get()); @@ -1162,7 +1164,7 @@ public void CanBindImmutableClass_ThrowsOnMultipleParameterizedConstructors() // If the immutable type has a parameterized constructor, then throw // that constructor has an 'in' parameter - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnInParameter() { var dic = new Dictionary @@ -1176,7 +1178,7 @@ public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnInParame configurationBuilder.AddInMemoryCollection(dic); var config = configurationBuilder.Build(); - string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Tests.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithInParameter", "string1"); + string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, typeof(ImmutableClassWithOneParameterizedConstructorButWithInParameter), "string1"); var ex = Assert.Throws(() => config.Get()); @@ -1185,7 +1187,7 @@ public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnInParame // If the immutable type has a parameterized constructors, then throw // that constructor has a 'ref' parameter - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithARefParameter() { var dic = new Dictionary @@ -1199,7 +1201,7 @@ public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithARefParame configurationBuilder.AddInMemoryCollection(dic); var config = configurationBuilder.Build(); - string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Tests.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithRefParameter", "int1"); + string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, typeof(ImmutableClassWithOneParameterizedConstructorButWithRefParameter), "int1"); var ex = Assert.Throws(() => config.Get()); @@ -1208,7 +1210,7 @@ public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithARefParame // If the immutable type has a parameterized constructors, then throw // if the constructor has an 'out' parameter - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnOutParameter() { var dic = new Dictionary @@ -1222,14 +1224,14 @@ public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnOutParam configurationBuilder.AddInMemoryCollection(dic); var config = configurationBuilder.Build(); - string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Tests.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithOutParameter", "int2"); + string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, typeof(ImmutableClassWithOneParameterizedConstructorButWithOutParameter), "int2"); var ex = Assert.Throws(() => config.Get()); Assert.Equal(expectedMessage, ex.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]// Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void CanBindMutableStruct_UnmatchedConstructorsAreIgnored() { var dic = new Dictionary @@ -1324,6 +1326,23 @@ public void CanBindRecordOptions() Assert.Equal("Green", options.Color); } + [Fact] + public void CanBindClassWithPrimaryCtor() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + [Fact] public void CanBindRecordStructOptions() { @@ -1364,7 +1383,7 @@ public void CanBindNestedRecordOptions() Assert.Equal(24, options.Nested2.ValueB); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure parameterized ctor support exception messages & param-property match validation are in sync. + [Fact] public void CanBindOnParametersAndProperties_PropertiesAreSetAfterTheConstructor() { var dic = new Dictionary diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt index 5c7c635dac9ace..53e5174c18f943 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt @@ -5,7 +5,7 @@ internal static class GeneratedConfigurationBinder { public static void Bind(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::Program.MyClass obj) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.BindCore(configuration, ref obj, binderOptions: null); - public static void Bind(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::Program.MyClass obj, global::System.Action? configureActions) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.BindCore(configuration, ref obj, global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetBinderOptions(configureActions)); + public static void Bind(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::Program.MyClass obj, global::System.Action? configureOptions) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.BindCore(configuration, ref obj, global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetBinderOptions(configureOptions)); public static void Bind(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, string key, global::Program.MyClass obj) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.BindCore(configuration.GetSection(key), ref obj, binderOptions: null); } @@ -56,6 +56,11 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration public static void BindCore(IConfiguration configuration, ref Program.MyClass2 obj, BinderOptions? binderOptions) { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + } public static void BindCore(IConfiguration configuration, ref Dictionary obj, BinderOptions? binderOptions) @@ -164,14 +169,14 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration return false; } - public static BinderOptions? GetBinderOptions(System.Action? configureActions) + public static BinderOptions? GetBinderOptions(System.Action? configureOptions) { - if (configureActions is null) + if (configureOptions is null) { return null; } BinderOptions binderOptions = new(); - configureActions(binderOptions); + configureOptions(binderOptions); if (binderOptions.BindNonPublicProperties) { throw new global::System.NotSupportedException($"The configuration binding source generator does not support 'BinderOptions.BindNonPublicProperties'."); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt index e8508b3946c6ff..e10a7f436cf755 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt @@ -3,7 +3,7 @@ internal static class GeneratedConfigurationBinder { - public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureActions: null) ?? default(T)); + public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureOptions: null) ?? default(T)); } namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration @@ -16,14 +16,14 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration internal static class Helpers { - public static object? GetCore(this IConfiguration configuration, Type type, Action? configureActions) + public static object? GetCore(this IConfiguration configuration, Type type, Action? configureOptions) { if (configuration is null) { throw new ArgumentNullException(nameof(configuration)); } - BinderOptions? binderOptions = GetBinderOptions(configureActions); + BinderOptions? binderOptions = GetBinderOptions(configureOptions); if (!HasValueOrChildren(configuration)) { @@ -156,14 +156,6 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration } } break; - case "ICustomDictionary": - { - } - break; - case "ICustomCollection": - { - } - break; case "IReadOnlyList": { if (HasChildren(section)) @@ -175,10 +167,6 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration } } break; - case "UnsupportedIReadOnlyDictionaryUnsupported": - { - } - break; case "IReadOnlyDictionary": { if (HasChildren(section)) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt index c9c2c5626e143c..53be83073d395d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt @@ -3,13 +3,13 @@ internal static class GeneratedConfigurationBinder { - public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureActions: null) ?? default(T)); + public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureOptions: null) ?? default(T)); - public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Action? configureActions) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureActions) ?? default(T)); + public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Action? configureOptions) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureOptions) ?? default(T)); - public static object? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Type type) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, type, configureActions: null); + public static object? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Type type) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, type, configureOptions: null); - public static object? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Type type, global::System.Action? configureActions) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, type, configureActions); + public static object? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Type type, global::System.Action? configureOptions) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, type, configureOptions); } namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration @@ -21,14 +21,14 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration internal static class Helpers { - public static object? GetCore(this IConfiguration configuration, Type type, Action? configureActions) + public static object? GetCore(this IConfiguration configuration, Type type, Action? configureOptions) { if (configuration is null) { throw new ArgumentNullException(nameof(configuration)); } - BinderOptions? binderOptions = GetBinderOptions(configureActions); + BinderOptions? binderOptions = GetBinderOptions(configureOptions); if (!HasValueOrChildren(configuration)) { @@ -234,14 +234,14 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration return false; } - public static BinderOptions? GetBinderOptions(System.Action? configureActions) + public static BinderOptions? GetBinderOptions(System.Action? configureOptions) { - if (configureActions is null) + if (configureOptions is null) { return null; } BinderOptions binderOptions = new(); - configureActions(binderOptions); + configureOptions(binderOptions); if (binderOptions.BindNonPublicProperties) { throw new global::System.NotSupportedException($"The configuration binding source generator does not support 'BinderOptions.BindNonPublicProperties'."); From 33952ca476a864583a752ab48ad0d858861335b0 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Fri, 26 May 2023 12:15:00 -0700 Subject: [PATCH 3/3] Fix ctor preference logic and enable passing test --- .../ConfigurationBindingGenerator.Parser.cs | 6 ++- .../tests/Common/ConfigurationBinderTests.cs | 50 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) 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 d705cc0ad59d19..020c1104018669 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Operations; @@ -832,7 +833,10 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc objectSpec.InitExceptionMessage = string.Format(ExceptionMessages.MultipleParameterizedConstructors, typeName); } - ctor = parameterizedCtor ?? parameterlessCtor; + ctor = type.IsValueType + // Roslyn ctor fetching APIs include paramerterless ctors for structs, unlike System.Reflection. + ? parameterizedCtor ?? parameterlessCtor + : parameterlessCtor ?? parameterizedCtor; } objectSpec.InitializationStrategy = ctor?.Parameters.Length is 0 ? InitializationStrategy.ParameterlessConstructor : InitializationStrategy.ParameterizedConstructor; 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 ea3f402527b893..ec29f212b60f4e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -7,7 +7,9 @@ using System.Globalization; using System.Linq; using System.Reflection; +#if BUILDING_SOURCE_GENERATOR_TESTS using Microsoft.Extensions.Configuration; +#endif using Microsoft.Extensions.Configuration.Test; using Xunit; @@ -903,7 +905,7 @@ public void ExceptionWhenTryingToBindToInterface() exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync + [Fact] public void ExceptionWhenTryingToBindClassWithoutParameterlessConstructor() { var input = new Dictionary @@ -1824,5 +1826,51 @@ public void TypeWithPrimitives_Pass() Assert.Equal(TimeOnly.Parse("18:26:38.7327436"), obj.Prop22); #endif } + + [Fact] + public void ForClasses_ParameterlessConstructorIsPickedOverParameterized() + { + string data = """ + { + "MyInt": 9, + } + """; + + var configuration = TestHelpers.GetConfigurationFromJsonString(data); + var obj = configuration.Get(); + Assert.Equal(1, obj.MyInt); + } + + [Fact] + public void ForStructs_ParameterlessConstructorIsPickedOverParameterized() + { + string data = """ + { + "MyInt": 10, + } + """; + + var configuration = TestHelpers.GetConfigurationFromJsonString(data); + var obj = configuration.Get(); + Assert.Equal(1, obj.MyInt); + } + + public class ClassWithParameterlessAndParameterizedCtor + { + public ClassWithParameterlessAndParameterizedCtor() => MyInt = 1; + + public ClassWithParameterlessAndParameterizedCtor(int myInt) => MyInt = 10; + + public int MyInt { get; } + } + + public struct StructWithParameterlessAndParameterizedCtor + { + public StructWithParameterlessAndParameterizedCtor() => MyInt = 1; + + public StructWithParameterlessAndParameterizedCtor(int myInt) => MyInt = 10; + + public int MyInt { get; } + } } }