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..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,6 +73,7 @@ public void Emit() EmitGetCoreMethod(); EmitGetValueCoreMethod(); EmitBindCoreMethods(); + EmitInitializeMethods(); EmitHelperMethods(); _writer.WriteBlockEnd(); // End helper class. @@ -93,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; @@ -121,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});"); } } @@ -186,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); } } @@ -203,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); } } @@ -214,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 @@ -238,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(); } @@ -282,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}))"); @@ -308,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) { @@ -322,12 +347,169 @@ 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 EmitInitializeMethods() + { + if (!_generationSpec.ShouldEmitMethods(BinderMethodSpecifier.Initialize)) + { + return; + } + + foreach (ObjectSpec type in _generationSpec.ConfigTypes[BinderMethodSpecifier.Initialize]) + { + EmitBlankLineIfRequired(); + EmitInitializeMethod(type); + } + } + + private void EmitInitializeMethod(ObjectSpec type) + { + 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.InitializeMethodDisplayString}({Identifier.IConfiguration} {Identifier.configuration}, {Identifier.BinderOptions}? {Identifier.binderOptions})"); + + foreach (ParameterSpec parameter in ctorParams) + { + 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 initOnlyProps) + { + if (property.MatchingCtorParam is null) + { + _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 ctorParams) + { + EmitMemberBindLogic(parameter.Name, parameter.Type, parameter.ConfigurationKeyName, configValueMustExist: !parameter.HasExplicitDefaultValue); + argumentList.Add(GetExpressionForArgument(parameter)); + } + + foreach (PropertySpec property in initOnlyProps) + { + if (property.ShouldBind() && property.MatchingCtorParam is null) + { + EmitMemberBindLogic(property.Name, property.Type, property.ConfigurationKeyName); + } + } + + _writer.WriteBlock(""" + default: + { + continue; + } + } + } + """); + + _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($"{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(";"); + } + + // End method. + _writer.WriteBlockEnd(); + + #region Local helpers + 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, lhs); + + if (configValueMustExist) + { + _writer.WriteLine($"{memberName}.{Identifier.HasConfig} = true;"); + } + + _writer.WriteBlockEnd(); + _writer.WriteLine("break;"); + + void EmitMemberBindLogicCore(TypeSpec type, string lhs) + { + TypeSpecKind kind = type.SpecKind; + + if (kind is TypeSpecKind.Nullable) + { + EmitMemberBindLogicCore(((NullableSpec)type).UnderlyingType, lhs); + } + else if (kind is TypeSpecKind.ParsableFromString) + { + EmitBindLogicFromString((ParsableFromStringSpec)type, lhs, Expression.sectionValue, Expression.sectionPath); + } + else if (!EmitInitException(type)) + { + EmitBindCoreCall(type, lhs, Identifier.section, InitializationKind.SimpleAssignment); + } + } + } + #endregion + } + private void EmitHelperMethods() { if (_generationSpec.ShouldEmitMethods(BinderMethodSpecifier.Get | BinderMethodSpecifier.Configure)) @@ -393,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) { @@ -487,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}}) { @@ -503,7 +683,7 @@ private void EmitPrimitiveParseMethod(ParsableFromStringSpec type) } catch ({{innerExceptionTypeDisplayString}} {{Identifier.exception}}) { - throw new {{exceptionTypeDisplayString}}($"{{exceptionArg1}}", {{Identifier.exception}}); + throw new {{GetInvalidOperationDisplayName()}}($"{{exceptionArg1}}", {{Identifier.exception}}); } } """); @@ -516,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: @@ -531,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((EnumerableSpec)type); + } + else if (type is EnumerableSpec enumerable) + { + EmitPopulationImplForEnumerableWithAdd(enumerable); + } + else if (type is DictionarySpec dictionary) { - EmitPopulationImplForArray(type); + EmitBindCoreImplForDictionary(dictionary); } else { - EmitPopulationImplForEnumerableWithAdd(type); + EmitBindCoreImplForObject((ObjectSpec)type); } } @@ -614,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) @@ -665,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 : "?"); @@ -684,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!}"; @@ -712,31 +893,32 @@ void Emit_BindAndAddLogic_ForElement() private void EmitBindCoreImplForObject(ObjectSpec type) { - Dictionary properties = type.Properties; - 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($$""" @@ -768,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; @@ -793,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};"); @@ -814,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) @@ -828,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); } } @@ -853,6 +1037,8 @@ private void EmitBindCoreCall( string expressionForConfigArg, InitializationKind initKind) { + Debug.Assert(type.CanInitialize); + string tempVarName = GetIncrementalVarName(Identifier.temp); if (initKind is InitializationKind.AssignmentWithNullCheck) { @@ -921,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(); } @@ -968,38 +1157,42 @@ 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; CollectionSpec? collectionType = type as CollectionSpec; - string typeDisplayString; + string effectiveDisplayString = GetTypeDisplayString(type); if (collectionType is not null) { - if (collectionType is EnumerableSpec { PopulationStrategy: CollectionPopulationStrategy.Array }) + if (collectionType is EnumerableSpec { InitializationStrategy: InitializationStrategy.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) + else if (type.InitializationStrategy is InitializationStrategy.ParameterlessConstructor) { - typeDisplayString = GetTypeDisplayString(type); - expressionForInit = $"new {typeDisplayString}()"; + expressionForInit = $"new {effectiveDisplayString}()"; } 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) @@ -1009,14 +1202,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 + { + InitializationStrategy: InitializationStrategy.ParameterizedConstructor or InitializationStrategy.ToEnumerableMethod + }) { - _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : {expressionForMemberAccess}.{collectionType.ToEnumerableMethodCall!};"); + if (collectionType.InitializationStrategy is InitializationStrategy.ParameterizedConstructor) + { + _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : new {effectiveDisplayString}({expressionForMemberAccess});"); + } + else + { + _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : {expressionForMemberAccess}.{collectionType.ToEnumerableMethodCall!};"); + } } else { @@ -1025,8 +1223,11 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini } else { + 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 c1678248f0a19a..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; @@ -19,7 +20,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); @@ -28,6 +29,7 @@ private sealed class Parser private readonly HashSet _typeNamespaces = new() { "System", + "System.Collections.Generic", "System.Globalization", "Microsoft.Extensions.Configuration" }; @@ -83,10 +85,10 @@ public Parser(SourceProductionContext context, KnownTypeSymbols typeSymbols) } return new SourceGenerationSpec( - _rootConfigTypes, + _configTypes, _methodsToGen, _primitivesForHelperGen, - _typeNamespaces); + _typeNamespaces.ToImmutableSortedSet()); } private void ProcessBindCall(BinderInvocationOperation binderOperation) @@ -318,23 +320,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); - - _methodsToGen |= overload; + RegisterConfigType(spec, overload); + RegisterConfigType(spec, methodGroup, isMethodGroup: true); } - - return spec; } private TypeSpec? GetOrCreateTypeSpec(ITypeSymbol type, Location? location = null) @@ -344,6 +341,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) @@ -397,29 +396,37 @@ 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); - _methodsToGen |= BinderMethodSpecifier.BindCore; + if (spec.CanInitialize) + { + canInitialize = true; + RegisterConfigType(spec, BinderMethodSpecifier.BindCore); + } } } } - private void AddToRootConfigTypeCache(BinderMethodSpecifier method, TypeSpec spec) + 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 (!isMethodGroup) + { + _methodsToGen |= binderMethod; + } } private static bool IsNullable(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? underlyingType) @@ -560,23 +567,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) @@ -597,12 +603,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) @@ -619,15 +636,15 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc return null; } - ConstructionStrategy constructionStrategy; + InitializationStrategy constructionStrategy; CollectionPopulationStrategy populationStrategy; INamedTypeSymbol? concreteType = null; INamedTypeSymbol? populationCastType = null; string? toEnumerableMethodCall = null; - if (HasPublicParameterlessCtor(type)) + if (HasPublicParameterLessCtor(type)) { - constructionStrategy = ConstructionStrategy.ParameterlessConstructor; + constructionStrategy = InitializationStrategy.ParameterlessConstructor; if (HasAddMethod(type, keyType, elementType)) { @@ -647,14 +664,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"); @@ -670,14 +687,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; } @@ -690,14 +707,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)) + if (HasPublicParameterLessCtor(type)) { - constructionStrategy = ConstructionStrategy.ParameterlessConstructor; + constructionStrategy = InitializationStrategy.ParameterlessConstructor; if (HasAddMethod(type, elementType)) { @@ -718,34 +735,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 @@ -760,73 +777,174 @@ 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)); - // Add spec to cache before traversing properties to avoid stack overflow. - if (!HasPublicParameterlessCtor(type)) + ObjectSpec objectSpec = new(type) { Location = location }; + _createdSpecs.Add(type, objectSpec); + + string typeName = objectSpec.Name; + IMethodSymbol? ctor = null; + DiagnosticDescriptor? diagnosticDescriptor = null; + + if (!(type.IsAbstract || type.TypeKind is TypeKind.Interface)) { - ReportUnsupportedType(type, ParserDiagnostics.NeedPublicParameterlessConstructor, location); - _createdSpecs.Add(type, null); - return null; + IMethodSymbol? parameterlessCtor = null; + IMethodSymbol? parameterizedCtor = null; + bool hasMultipleParameterizedCtors = false; + + foreach (IMethodSymbol candidate in type.InstanceConstructors) + { + if (candidate.DeclaredAccessibility is not Accessibility.Public) + { + continue; + } + + if (candidate.Parameters.Length is 0) + { + parameterlessCtor = candidate; + } + else if (parameterizedCtor is not null) + { + hasMultipleParameterizedCtors = true; + } + else + { + parameterizedCtor = candidate; + } + } + + bool hasPublicParameterlessCtor = type.IsValueType || parameterlessCtor is not null; + if (!hasPublicParameterlessCtor && hasMultipleParameterizedCtors) + { + diagnosticDescriptor = ParserDiagnostics.MultipleParameterizedConstructors; + objectSpec.InitExceptionMessage = string.Format(ExceptionMessages.MultipleParameterizedConstructors, typeName); + } + + 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; + + 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; } - ObjectSpec objectSpec = new(type) { Location = location, ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor }; - _createdSpecs.Add(type, 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(property.Type); + if (propertyTypeSpec is null) + { + _context.ReportDiagnostic(Diagnostic.Create(ParserDiagnostics.PropertyNotSupported, location, new string[] { propertyName, type.ToDisplayString() })); + } + else { - 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; + PropertySpec spec = new(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; + objectSpec.Properties[propertyName] = spec; + RegisterHasChildrenHelperForGenIfRequired(propertyTypeSpec); + } + } + } + current = current.BaseType; + } - TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(propertyType); - PropertySpec spec; + if (objectSpec.InitializationStrategy is InitializationStrategy.ParameterizedConstructor) + { + List missingParameters = new(); + List invalidParameters = new(); - if (propertyTypeSpec is null) - { - _context.ReportDiagnostic(Diagnostic.Create(ParserDiagnostics.PropertyNotSupported, location, new string[] { propertyName, type.ToDisplayString() })); - } - else - { - RegisterHasChildrenHelperForGenIfRequired(propertyTypeSpec); - } + 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, + }; - spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; - objectSpec.Properties[configKeyName] = spec; - } + propertySpec.MatchingCtorParam = paramSpec; + objectSpec.ConstructorParameters.Add(paramSpec); } } - current = current.BaseType; + + 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) { @@ -921,28 +1039,8 @@ public static bool ContainsGenericParameters(INamedTypeSymbol type) return false; } - private static bool HasPublicParameterlessCtor(INamedTypeSymbol type) - { - if (type.IsAbstract || type.TypeKind == TypeKind.Interface) - { - return false; - } - - if (type is not INamedTypeSymbol namedType) - { - return false; - } - - foreach (IMethodSymbol ctor in namedType.InstanceConstructors) - { - if (ctor.DeclaredAccessibility == Accessibility.Public && ctor.Parameters.Length == 0) - { - return true; - } - } - - return false; - } + private static bool HasPublicParameterLessCtor(INamedTypeSymbol type) => + 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) { @@ -979,7 +1077,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 2f2e29690f4fd0..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 @@ -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..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,6 +74,7 @@ internal enum BinderMethodSpecifier // Binding helpers BindCore = 0x1000, HasChildren = 0x4000, + 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 046d521a087225..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; @@ -8,10 +9,21 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { internal sealed record ObjectSpec : TypeSpec { - public ObjectSpec(INamedTypeSymbol type) : base(type) { } + public ObjectSpec(INamedTypeSymbol type) : base(type) + { + InitializeMethodDisplayString = $"Initialize{type.Name.Replace(".", string.Empty).Replace("<", string.Empty).Replace(">", string.Empty)}"; + } public override TypeSpecKind SpecKind => TypeSpecKind.Object; - public Dictionary Properties { get; } = new(); + public override InitializationStrategy InitializationStrategy { get; set; } + + public override bool CanInitialize => CanInitCompexType; + + public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); + + public List ConstructorParameters { get; } = new(); + + 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 new file mode 100644 index 00000000000000..a62f6080537ba2 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/ParameterSpec.cs @@ -0,0 +1,36 @@ +// 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 Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + internal sealed record ParameterSpec + { + public ParameterSpec(IParameterSymbol parameter) + { + Name = parameter.Name; + RefKind = parameter.RefKind; + + HasExplicitDefaultValue = parameter.HasExplicitDefaultValue; + if (HasExplicitDefaultValue) + { + string formatted = SymbolDisplay.FormatPrimitive(parameter.ExplicitDefaultValue, quoteStrings: true, useHexadecimalNumbers: false); + DefaultValue = formatted is "null" ? "default!" : formatted; + } + } + + 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 db8e1aaccce1c5..f33a2980d4d5b4 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Model/SourceGenerationSpec.cs @@ -2,14 +2,15 @@ // 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 { internal sealed record SourceGenerationSpec( - Dictionary> RootConfigTypes, + Dictionary> ConfigTypes, 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..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; init; } + 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 c84d33bf8a4d2c..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; @@ -19,7 +21,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() @@ -42,6 +44,8 @@ public void BindWithNestedTypesWithReadOnlyProperties() Assert.Equal("Dummy", result.Nested.MyProp); } + // Add test for type with parameterless ctor + init-only properties. + [Fact] public void EnumBindCaseInsensitiveNotThrows() { @@ -901,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 @@ -920,8 +924,8 @@ public void ExceptionWhenTryingToBindClassWithoutParameterlessConstructor() exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. - public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParameters() + [Fact] + public void ExceptionWhenTryingToBindClassWherePropertiesDoNotMatchConstructorParameters() { var input = new Dictionary { @@ -941,7 +945,7 @@ public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParam exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() { var input = new Dictionary @@ -961,7 +965,7 @@ public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor() { var input = new Dictionary @@ -982,7 +986,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 +1007,7 @@ public void BindsToClassConstructorParametersWithDefaultValues() Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.Age); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void FieldsNotSupported_ExceptionBindingToConstructorWithParameterMatchingAField() { var input = new Dictionary @@ -1025,7 +1029,7 @@ public void FieldsNotSupported_ExceptionBindingToConstructorWithParameterMatchin exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void BindsToRecordPrimaryConstructorParametersWithDefaultValues() { var input = new Dictionary @@ -1101,7 +1105,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 +1122,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 +1143,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. + [Fact] public void CanBindImmutableClass_ThrowsOnMultipleParameterizedConstructors() { var dic = new Dictionary @@ -1153,7 +1157,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 +1166,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. + [Fact] public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnInParameter() { var dic = new Dictionary @@ -1176,7 +1180,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 +1189,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. + [Fact] public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithARefParameter() { var dic = new Dictionary @@ -1199,7 +1203,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 +1212,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. + [Fact] public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnOutParameter() { var dic = new Dictionary @@ -1222,14 +1226,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))] // Need support for parameterized ctors. + [Fact] public void CanBindMutableStruct_UnmatchedConstructorsAreIgnored() { var dic = new Dictionary @@ -1248,7 +1252,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 +1273,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 +1292,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 +1311,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 +1328,24 @@ public void CanBindRecordOptions() Assert.Equal("Green", options.Color); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [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() { var dic = new Dictionary @@ -1341,7 +1362,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 +1385,7 @@ public void CanBindNestedRecordOptions() Assert.Equal(24, options.Nested2.ValueB); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + [Fact] public void CanBindOnParametersAndProperties_PropertiesAreSetAfterTheConstructor() { var dic = new Dictionary @@ -1381,7 +1402,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 @@ -1805,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; } + } } } 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..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,17 +5,17 @@ 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); } 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 { @@ -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 cba992ebfc1bb6..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,27 +3,27 @@ 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 { - 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 { - 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/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..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,32 +3,32 @@ 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 { - using System; - using System.Globalization; using Microsoft.Extensions.Configuration; + using System; using System.Collections.Generic; + using System.Globalization; 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'."); 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 {