diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs index 31058c49a009f..f86f66803be63 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs @@ -22,11 +22,18 @@ public CollectionSpec(ITypeSymbol type) : base(type) public CollectionSpec? ConcreteType { get; init; } } + internal sealed record ArraySpec : CollectionSpec + { + public ArraySpec(ITypeSymbol type) : base(type) { } + + public override TypeSpecKind SpecKind => TypeSpecKind.Array; + } + internal sealed record EnumerableSpec : CollectionSpec { public EnumerableSpec(ITypeSymbol type) : base(type) { } - public override TypeSpecKind SpecKind { get; init; } = TypeSpecKind.Enumerable; + public override TypeSpecKind SpecKind => TypeSpecKind.Enumerable; } internal sealed record DictionarySpec : CollectionSpec @@ -35,6 +42,6 @@ public DictionarySpec(INamedTypeSymbol type) : base(type) { } public override TypeSpecKind SpecKind => TypeSpecKind.Dictionary; - public required TypeSpec KeyType { get; init; } + public required ParsableFromStringTypeSpec KeyType { get; init; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs index 046b518f70a45..4e33574162cab 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs @@ -17,6 +17,7 @@ private static class Expression { public const string nullableSectionValue = "section?.Value"; public const string sectionKey = "section.Key"; + public const string sectionPath = "section.Path"; public const string sectionValue = "section.Value"; public const string ConvertFromBase64String = "Convert.FromBase64String"; @@ -25,12 +26,16 @@ private static class Expression private static class FullyQualifiedDisplayName { public const string ArgumentNullException = "global::System.ArgumentNullException"; + public const string CultureInfo = "global::System.Globalization.CultureInfo"; + public const string CultureNotFoundException = "global::System.Globalization.CultureNotFoundException"; + public const string FormatException = "global::System.FormatException"; public const string Helpers = $"global::{GeneratorProjectName}.{Identifier.Helpers}"; public const string IConfiguration = "global::Microsoft.Extensions.Configuration.IConfiguration"; public const string IConfigurationSection = IConfiguration + "Section"; public const string InvalidOperationException = "global::System.InvalidOperationException"; public const string IServiceCollection = "global::Microsoft.Extensions.DependencyInjection.IServiceCollection"; public const string NotSupportedException = "global::System.NotSupportedException"; + public const string NumberStyles = "global::System.Globalization.NumberStyles"; } private enum InitializationKind @@ -99,7 +104,7 @@ private void EmitConfigureMethod() _writer.WriteBlockStart($@"return {Identifier.services}.{Identifier.Configure}<{typeDisplayString}>({Identifier.obj} =>"); EmitIConfigurationHasValueOrChildrenCheck(); - EmitBindLogicFromIConfiguration(type, Identifier.obj, InitializationKind.None); + EmitBindLogicFromRootMethod(type, Identifier.obj, InitializationKind.None); _writer.WriteBlockEnd(");"); _writer.WriteBlockEnd(); @@ -133,7 +138,7 @@ private void EmitGetMethod() string typeDisplayString = type.FullyQualifiedDisplayString; _writer.WriteBlockStart($"if (typeof(T) == typeof({typeDisplayString}))"); - EmitBindLogicFromIConfiguration(type, Identifier.obj, InitializationKind.Declaration); + EmitBindLogicFromRootMethod(type, Identifier.obj, InitializationKind.Declaration); _writer.WriteLine($"return (T)(object){Identifier.obj};"); _writer.WriteBlockEnd(); _writer.WriteBlankLine(); @@ -223,13 +228,12 @@ private void EmitBindCoreImpl(TypeSpec type) { case TypeSpecKind.Array: { - EmitBindCoreImplForArray((type as EnumerableSpec)!); + EmitBindCoreImplForArray((type as ArraySpec)!); } break; - case TypeSpecKind.IConfigurationSection: + case TypeSpecKind.Enumerable: { - EmitCastToIConfigurationSection(); - EmitAssignment(Identifier.obj, Identifier.section); + EmitBindCoreImplForEnumerable((type as EnumerableSpec)!); } break; case TypeSpecKind.Dictionary: @@ -237,9 +241,10 @@ private void EmitBindCoreImpl(TypeSpec type) EmitBindCoreImplForDictionary((type as DictionarySpec)!); } break; - case TypeSpecKind.Enumerable: + case TypeSpecKind.IConfigurationSection: { - EmitBindCoreImplForEnumerable((type as EnumerableSpec)!); + EmitCastToIConfigurationSection(); + EmitAssignment(Identifier.obj, Identifier.section); } break; case TypeSpecKind.Object: @@ -258,16 +263,15 @@ private void EmitBindCoreImpl(TypeSpec type) } } - private void EmitBindCoreImplForArray(EnumerableSpec type) + private void EmitBindCoreImplForArray(ArraySpec type) { EnumerableSpec concreteType = (type.ConcreteType as EnumerableSpec)!; Debug.Assert(type.SpecKind == TypeSpecKind.Array && type.ConcreteType is not null); EmitCheckForNullArgument_WithBlankLine_IfRequired(isValueType: false); + // Create, bind, and add elements to temp list. string tempVarName = GetIncrementalVarName(Identifier.temp); - - // Create and bind to temp list EmitBindCoreCall(concreteType, tempVarName, Identifier.configuration, InitializationKind.Declaration); // Resize array and copy fill with additional @@ -278,95 +282,115 @@ private void EmitBindCoreImplForArray(EnumerableSpec type) """); } - private void EmitBindCoreImplForDictionary(DictionarySpec type) + private void EmitBindCoreImplForEnumerable(EnumerableSpec type) { EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType); - TypeSpec keyType = type.KeyType; TypeSpec elementType = type.ElementType; - EmitVarDeclaration(keyType, Identifier.key); - _writer.WriteBlockStart($"foreach ({Identifier.IConfigurationSection} {Identifier.section} in {Identifier.configuration}.{Identifier.GetChildren}())"); + _writer.WriteBlockStart($"if ({Identifier.HasValueOrChildren}({Identifier.section}))"); - // Parse key - EmitBindLogicFromString( - keyType, - Identifier.key, - expressionForConfigStringValue: Expression.sectionKey, - writeExtraOnSuccess: Emit_BindAndAddLogic_ForElement); + string addStatement = $"{Identifier.obj}.{Identifier.Add}({Identifier.element})"; - void Emit_BindAndAddLogic_ForElement() + if (elementType.SpecKind is TypeSpecKind.ParsableFromString) { - // For simple types: do regular dictionary add - if (elementType.SpecKind == TypeSpecKind.StringBasedParse) + ParsableFromStringTypeSpec stringParsableType = (ParsableFromStringTypeSpec)elementType; + if (stringParsableType.StringParseableTypeKind is StringParsableTypeKind.ConfigValue) { - EmitVarDeclaration(elementType, Identifier.element); - EmitBindLogicFromIConfigurationSectionValue( - elementType, - Identifier.element, - InitializationKind.SimpleAssignment, - writeExtraOnSuccess: () => EmitAssignment($"{Identifier.obj}[{Identifier.key}]", Identifier.element)); + string tempVarName = GetIncrementalVarName(Identifier.stringValue); + _writer.WriteBlockStart($"if ({Expression.sectionValue} is string {tempVarName})"); + _writer.WriteLine($"{Identifier.obj}.{Identifier.Add}({tempVarName});"); + _writer.WriteBlockEnd(); } - else // For complex types: + else { - string displayString = elementType.MinimalDisplayString + (elementType.IsValueType ? string.Empty : "?"); - - // If key already exists, bind to value to existing element instance if not null (for ref types) - string conditionToUseExistingElement = $"if ({Identifier.obj}.{Identifier.TryGetValue}({Identifier.key}, out {displayString} {Identifier.element})"; - conditionToUseExistingElement += !elementType.IsValueType - ? $" && {Identifier.element} is not null)" - : ")"; - _writer.WriteBlockStart(conditionToUseExistingElement); - EmitBindLogicForElement(InitializationKind.None); - _writer.WriteBlockEnd(); - - // Else, create new element instance and bind to that - _writer.WriteBlockStart("else"); - EmitBindLogicForElement(InitializationKind.SimpleAssignment); - _writer.WriteBlockEnd(); - - void EmitBindLogicForElement(InitializationKind initKind) - { - EmitBindLogicFromIConfigurationSectionValue(elementType, Identifier.element, initKind); - EmitAssignment($"{Identifier.obj}[{Identifier.key}]", Identifier.element); - } + EmitVarDeclaration(elementType, Identifier.element); + EmitBindLogicFromString(stringParsableType, Identifier.element, Expression.sectionValue, Expression.sectionPath, () => _writer.WriteLine($"{addStatement};")); } } + else + { + EmitBindCoreCall(elementType, Identifier.element, Identifier.section, InitializationKind.Declaration); + _writer.WriteLine($"{addStatement};"); + } - // End foreach loop. + _writer.WriteBlockEnd(); _writer.WriteBlockEnd(); } - private void EmitBindCoreImplForEnumerable(EnumerableSpec type) + private void EmitBindCoreImplForDictionary(DictionarySpec type) { EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType); - TypeSpec elementType = type.ElementType; - - EmitVarDeclaration(elementType, Identifier.element); _writer.WriteBlockStart($"foreach ({Identifier.IConfigurationSection} {Identifier.section} in {Identifier.configuration}.{Identifier.GetChildren}())"); + _writer.WriteBlockStart($"if ({Identifier.HasValueOrChildren}({Identifier.section}))"); - EmitBindLogicFromIConfigurationSectionValue( - elementType, - Identifier.element, - InitializationKind.SimpleAssignment, - writeExtraOnSuccess: EmitAddLogicForElement); + // Parse key + ParsableFromStringTypeSpec keyType = type.KeyType; - void EmitAddLogicForElement() + if (keyType.StringParseableTypeKind is StringParsableTypeKind.ConfigValue) + { + _writer.WriteLine($"{keyType.MinimalDisplayString} {Identifier.key} = {Expression.sectionKey};"); + Emit_BindAndAddLogic_ForElement(); + } + else { - string addExpression = $"{Identifier.obj}.{Identifier.Add}({Identifier.element})"; - if (elementType.IsValueType) + EmitVarDeclaration(keyType, Identifier.key); + EmitBindLogicFromString( + keyType, + Identifier.key, + expressionForConfigStringValue: Expression.sectionKey, + expressionForConfigValuePath: Expression.sectionValue, + writeOnSuccess: Emit_BindAndAddLogic_ForElement); + } + + void Emit_BindAndAddLogic_ForElement() + { + TypeSpec elementType = type.ElementType; + + if (elementType.SpecKind == TypeSpecKind.ParsableFromString) { - _writer.WriteLine($"{addExpression};"); + ParsableFromStringTypeSpec stringParsableType = (ParsableFromStringTypeSpec)elementType; + if (stringParsableType.StringParseableTypeKind is StringParsableTypeKind.ConfigValue) + { + string tempVarName = GetIncrementalVarName(Identifier.stringValue); + _writer.WriteBlockStart($"if ({Expression.sectionValue} is string {tempVarName})"); + _writer.WriteLine($"{Identifier.obj}[{Identifier.key}] = {tempVarName};"); + _writer.WriteBlockEnd(); + } + else + { + EmitVarDeclaration(elementType, Identifier.element); + EmitBindLogicFromString( + stringParsableType, + Identifier.element, + Expression.sectionValue, + Expression.sectionPath, + () => _writer.WriteLine($"{Identifier.obj}[{Identifier.key}] = {Identifier.element};")); + } } - else + else // For complex types: { - _writer.WriteLine($"if ({Identifier.element} is not null) {{ {addExpression}; }}"); + string elementTypeDisplayString = elementType.MinimalDisplayString + (elementType.IsValueType ? string.Empty : "?"); + + // If key already exists, bind to value to existing element instance if not null (for ref types). + string conditionToUseExistingElement = $"{Identifier.obj}.{Identifier.TryGetValue}({Identifier.key}, out {elementTypeDisplayString} {Identifier.element})"; + if (!elementType.IsValueType) + { + conditionToUseExistingElement += $" && {Identifier.element} is not null"; + } + _writer.WriteBlockStart($"if (!({conditionToUseExistingElement}))"); + EmitObjectInit(elementType, Identifier.element, InitializationKind.SimpleAssignment); + _writer.WriteBlockEnd(); + + EmitBindCoreCall(elementType, $"{Identifier.element}!", Identifier.section, InitializationKind.None); + _writer.WriteLine($"{Identifier.obj}[{Identifier.key}] = {Identifier.element};"); } } _writer.WriteBlockEnd(); + _writer.WriteBlockEnd(); } private void EmitBindCoreImplForObject(ObjectSpec type) @@ -400,25 +424,19 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert string expressionForConfigSectionAccess = $@"{Identifier.configuration}.{Identifier.GetSection}(""{configurationKeyName}"")"; string expressionForConfigValueIndexer = $@"{Identifier.configuration}[""{configurationKeyName}""]"; - bool canGet = property.CanGet; bool canSet = property.CanSet; switch (propertyType.SpecKind) { - case TypeSpecKind.System_Object: - { - EmitAssignment(expressionForPropertyAccess, $"{expressionForConfigValueIndexer}!"); - } - break; - case TypeSpecKind.StringBasedParse: - case TypeSpecKind.ByteArray: + case TypeSpecKind.ParsableFromString: { if (canSet) { EmitBindLogicFromString( - propertyType, + (propertyType as ParsableFromStringTypeSpec)!, expressionForPropertyAccess, - expressionForConfigValueIndexer); + expressionForConfigValueIndexer, + expressionForConfigValuePath: $@"{expressionForConfigSectionAccess}.{Identifier.Path}"); } } break; @@ -454,9 +472,9 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert } } - private void EmitBindLogicFromIConfiguration(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind) + private void EmitBindLogicFromRootMethod(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind) { - if (type.SpecKind is TypeSpecKind.StringBasedParse or TypeSpecKind.ByteArray) + if (type.SpecKind is TypeSpecKind.ParsableFromString) { if (initKind is InitializationKind.Declaration) { @@ -467,7 +485,7 @@ private void EmitBindLogicFromIConfiguration(TypeSpec type, string expressionFor { EmitCastToIConfigurationSection(); } - EmitBindLogicFromString(type, expressionForMemberAccess, Expression.sectionValue); + EmitBindLogicFromString((type as ParsableFromStringTypeSpec)!, expressionForMemberAccess, Expression.sectionValue, Expression.sectionPath); } else { @@ -475,19 +493,6 @@ private void EmitBindLogicFromIConfiguration(TypeSpec type, string expressionFor } } - private void EmitBindLogicFromIConfigurationSectionValue(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind, Action? writeExtraOnSuccess = null) - { - if (type.SpecKind is TypeSpecKind.StringBasedParse or TypeSpecKind.ByteArray) - { - EmitBindLogicFromString(type, expressionForMemberAccess, Expression.sectionValue, writeExtraOnSuccess); - } - else - { - EmitBindCoreCall(type, expressionForMemberAccess, Identifier.section, initKind); - writeExtraOnSuccess?.Invoke(); - } - } - private void EmitBindCoreCall( TypeSpec type, string expressionForMemberAccess, @@ -588,49 +593,127 @@ private void EmitBindCoreCallForProperty( } private void EmitBindLogicFromString( - TypeSpec type, + ParsableFromStringTypeSpec type, string expressionForMemberAccess, string expressionForConfigStringValue, - Action? writeExtraOnSuccess = null) + string expressionForConfigValuePath, + Action? writeOnSuccess = null) { - string typeDisplayString = type.FullyQualifiedDisplayString; + StringParsableTypeKind typeKind = type.StringParseableTypeKind; + string typeDisplayString = GetTypeDisplayString(type); + string stringValueVarName = GetIncrementalVarName(Identifier.stringValue); - string assignmentCondition = $"{expressionForConfigStringValue} is string {stringValueVarName}"; - string rhs; - if (type.SpecialType != SpecialType.None) + string innerExceptionTypeDisplayString; + string cultureInfoTypeDisplayString; + string numberStylesTypeDisplayString; + if (_useFullyQualifiedNames) { - rhs = type.SpecialType switch - { - SpecialType.System_String => stringValueVarName, - SpecialType.System_Object => "default", - _ => $"{typeDisplayString}.{Identifier.Parse}({stringValueVarName})" - }; + innerExceptionTypeDisplayString = FullyQualifiedDisplayName.FormatException; + cultureInfoTypeDisplayString = FullyQualifiedDisplayName.CultureInfo; + numberStylesTypeDisplayString = FullyQualifiedDisplayName.NumberStyles; } - else if (type.SpecKind == TypeSpecKind.Enum) + else { - string enumValueVarName = GetIncrementalVarName(Identifier.enumValue); - assignmentCondition += $" && {Identifier.Enum}.{Identifier.TryParse}({stringValueVarName}, true, out {typeDisplayString} {enumValueVarName})"; - rhs = enumValueVarName; + innerExceptionTypeDisplayString = Identifier.Exception; + cultureInfoTypeDisplayString = Identifier.CultureInfo; + numberStylesTypeDisplayString = Identifier.NumberStyles; } - else if (type.SpecKind == TypeSpecKind.ByteArray) + + _writer.WriteBlockStart($"if ({expressionForConfigStringValue} is string {stringValueVarName})"); + + if (typeKind is StringParsableTypeKind.ConfigValue) { - rhs = $"{Expression.ConvertFromBase64String}({stringValueVarName})"; + EmitAssignment(expressionForMemberAccess, stringValueVarName); + writeOnSuccess?.Invoke(); + _writer.WriteBlockEnd(); + return; } - else + else if (typeKind is StringParsableTypeKind.Uri) { + string uriVarName = GetIncrementalVarName(Identifier.temp); + _writer.WriteLine($"{Identifier.Uri}.{Identifier.TryCreate}({stringValueVarName}, {Identifier.UriKind}.{Identifier.RelativeOrAbsolute}, out {Identifier.Uri}? {uriVarName});"); + _writer.WriteBlock($$""" + if ({{uriVarName}} is not null) + { + {{expressionForMemberAccess}} = {{uriVarName}}; + } + """); + _writer.WriteBlockEnd(); return; } - _writer.WriteBlockStart($"if ({assignmentCondition})"); - EmitAssignment(expressionForMemberAccess, rhs); - writeExtraOnSuccess?.Invoke(); + // Types we catch exceptions for. + string rhs; + switch (typeKind) + { + case StringParsableTypeKind.Enum: + { + rhs = $"({typeDisplayString}){Identifier.Enum}.{Identifier.Parse}(typeof({typeDisplayString}), {stringValueVarName}, true)"; + } + break; + case StringParsableTypeKind.ByteArray: + { + rhs = $"{Expression.ConvertFromBase64String}({stringValueVarName})"; + } + break; + case StringParsableTypeKind.Integer: + case StringParsableTypeKind.Float: + { + string numberInfoKind = typeKind is StringParsableTypeKind.Integer ? Identifier.Integer : Identifier.Float; + rhs = $"{typeDisplayString}.{Identifier.Parse}({stringValueVarName}, {numberStylesTypeDisplayString}.{numberInfoKind}, {cultureInfoTypeDisplayString}.{Identifier.InvariantCulture})"; + } + break; + case StringParsableTypeKind.Parse: + { + rhs = $"{typeDisplayString}.{Identifier.Parse}({stringValueVarName})"; + } + break; + case StringParsableTypeKind.ParseInvariant: + { + rhs = $"{typeDisplayString}.{Identifier.Parse}({stringValueVarName}, {cultureInfoTypeDisplayString}.{Identifier.InvariantCulture})"; ; + } + break; + case StringParsableTypeKind.CultureInfo: + { + rhs = $"{cultureInfoTypeDisplayString}.{Identifier.GetCultureInfoByIetfLanguageTag}({stringValueVarName})"; + } + break; + default: + { + Debug.Fail("Invalid string parsable kind", typeKind.ToString()); + return; + } + } + + string exceptionTypeDisplayString = _useFullyQualifiedNames ? FullyQualifiedDisplayName.InvalidOperationException : Identifier.InvalidOperationException; + string exceptionArg1 = string.Format(ExceptionMessages.FailedBinding, $"{{{expressionForConfigValuePath}}}", $"{{typeof({typeDisplayString})}}"); + string exceptionExpression = $@"throw new {exceptionTypeDisplayString}($""{exceptionArg1}"", {Identifier.exception})"; + + _writer.WriteBlock($$""" + try + { + {{expressionForMemberAccess}} = {{rhs}}; + """); + + writeOnSuccess?.Invoke(); + + _writer.WriteBlock($$""" + } + catch ({{innerExceptionTypeDisplayString}} {{Identifier.exception}}) + { + {{exceptionExpression}}; + } + """); + _writer.WriteBlockEnd(); + + return; } private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind) { - if (initKind is InitializationKind.None or InitializationKind.None) + if (initKind is InitializationKind.None) { return; } @@ -638,9 +721,9 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini string displayString = GetTypeDisplayString(type); string expressionForInit = null; - if (type is EnumerableSpec { SpecKind: TypeSpecKind.Array } arrayType) + if (type is ArraySpec) { - expressionForInit = $"new {_arrayBracketsRegex.Replace(displayString, "[0]", 1)};"; + expressionForInit = $"new {_arrayBracketsRegex.Replace(displayString, "[0]", 1)}"; } else if (type.ConstructionStrategy != ConstructionStrategy.ParameterlessConstructor) { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs index 2394107a2f6d6..d0ea5e0576cbe 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs @@ -34,10 +34,24 @@ public sealed partial class ConfigurationBindingSourceGenerator defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); - // Unlike sourcegen warnings, exception messages should not be localized so we keep them in source. + // Runtime exception messages; not localized so we keep them in source. private static class ExceptionMessages { public const string TypeNotSupported = "Unable to bind to type '{0}': '{1}'"; + public const string FailedBinding = "Failed to convert configuration value at '{0}' to type '{1}'."; + } + + private static class NotSupportedReason + { + public const string AbstractOrInterfaceNotSupported = "Abstract or interface types are not supported"; + public const string NeedPublicParameterlessConstructor = "Only objects with public parameterless ctors are supported"; + public const string CollectionNotSupported = "The collection type is not supported"; + public const string DictionaryKeyNotSupported = "The dictionary key type is not supported"; + public const string ElementTypeNotSupported = "The collection element type is not supported"; + public const string MultiDimArraysNotSupported = "Multidimensional arrays are not supported."; + public const string NullableUnderlyingTypeNotSupported = "Nullable underlying type is not supported"; + public const string TypeNotDetectedAsInput = "Generator parser did not detect the type as input"; + public const string TypeNotSupported = "The type is not supported"; } private static class Identifier @@ -45,6 +59,7 @@ private static class Identifier public const string configuration = nameof(configuration); public const string element = nameof(element); public const string enumValue = nameof(enumValue); + public const string exception = nameof(exception); public const string key = nameof(key); public const string obj = nameof(obj); public const string originalCount = nameof(originalCount); @@ -64,10 +79,15 @@ private static class Identifier public const string CopyTo = nameof(CopyTo); public const string ContainsKey = nameof(ContainsKey); public const string Count = nameof(Count); + public const string CultureInfo = nameof(CultureInfo); + public const string CultureNotFoundException = nameof(CultureNotFoundException); public const string Enum = nameof(Enum); + public const string Exception = nameof(Exception); + public const string Float = nameof(Float); public const string GeneratedConfigurationBinder = nameof(GeneratedConfigurationBinder); public const string Get = nameof(Get); public const string GetChildren = nameof(GetChildren); + public const string GetCultureInfoByIetfLanguageTag = nameof(GetCultureInfoByIetfLanguageTag); public const string GetSection = nameof(GetSection); public const string HasChildren = nameof(HasChildren); public const string HasValueOrChildren = nameof(HasValueOrChildren); @@ -76,42 +96,49 @@ private static class Identifier public const string IConfiguration = nameof(IConfiguration); public const string IConfigurationSection = nameof(IConfigurationSection); public const string Int32 = "int"; + public const string Integer = nameof(Integer); + public const string InvalidOperationException = nameof(InvalidOperationException); + public const string InvariantCulture = nameof(InvariantCulture); public const string Length = nameof(Length); + public const string NumberStyles = nameof(NumberStyles); public const string Parse = nameof(Parse); + public const string Path = nameof(Path); + public const string RelativeOrAbsolute = nameof(RelativeOrAbsolute); public const string Resize = nameof(Resize); + public const string TryCreate = nameof(TryCreate); public const string TryGetValue = nameof(TryGetValue); public const string TryParse = nameof(TryParse); + public const string Uri = nameof(Uri); + public const string UriKind = nameof(UriKind); public const string Value = nameof(Value); } - private static class NotSupportedReason - { - public const string AbstractOrInterfaceNotSupported = "Abstract or interface types are not supported"; - public const string NeedPublicParameterlessConstructor = "Only objects with public parameterless ctors are supported"; - public const string CollectionNotSupported = "The collection type is not supported"; - public const string DictionaryKeyNotSupported = "The dictionary key type is not supported"; - public const string ElementTypeNotSupported = "The collection element type is not supported"; - public const string MultiDimArraysNotSupported = "Multidimensional arrays are not supported."; - public const string NullableUnderlyingTypeNotSupported = "Nullable underlying type is not supported"; - public const string TypeNotDetectedAsInput = "Generator parser did not detect the type as input"; - public const string TypeNotSupported = "The type is not supported"; - } - private static class TypeFullName { public const string ConfigurationKeyNameAttribute = "Microsoft.Extensions.Configuration.ConfigurationKeyNameAttribute"; + public const string CultureInfo = "System.Globalization.CultureInfo"; + public const string DateOnly = "System.DateOnly"; + public const string DateTimeOffset = "System.DateTimeOffset"; public const string Dictionary = "System.Collections.Generic.Dictionary`2"; public const string GenericIDictionary = "System.Collections.Generic.IDictionary`2"; + public const string Guid = "System.Guid"; + public const string Half = "System.Half"; public const string HashSet = "System.Collections.Generic.HashSet`1"; public const string IConfiguration = "Microsoft.Extensions.Configuration.IConfiguration"; public const string IConfigurationSection = "Microsoft.Extensions.Configuration.IConfigurationSection"; public const string IDictionary = "System.Collections.Generic.IDictionary"; + public const string Int128 = "System.Int128"; public const string ISet = "System.Collections.Generic.ISet`1"; public const string IServiceCollection = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; public const string List = "System.Collections.Generic.List`1"; + public const string TimeOnly = "System.TimeOnly"; + public const string TimeSpan = "System.TimeSpan"; + public const string UInt128 = "System.UInt128"; + public const string Uri = "System.Uri"; + public const string Version = "System.Version"; } - private static bool TypesAreEqual(ITypeSymbol first, ITypeSymbol second) + private static bool TypesAreEqual(ITypeSymbol first, ITypeSymbol? second) => first.Equals(second, SymbolEqualityComparer.Default); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs index 26ed3669ccdb7..6f98c6a3b41d7 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Operations; @@ -17,7 +18,7 @@ private sealed class Parser private const string GlobalNameSpaceString = ""; private readonly SourceProductionContext _context; - private readonly KnownTypeData _typeData; + private readonly KnownTypeSymbols _typeSymbols; private readonly HashSet _typesForBindMethodGen = new(); private readonly HashSet _typesForGetMethodGen = new(); @@ -30,19 +31,19 @@ private sealed class Parser private readonly HashSet _namespaces = new() { "System", - "System.Linq", + "System.Globalization", "Microsoft.Extensions.Configuration" }; - public Parser(SourceProductionContext context, KnownTypeData typeData) + public Parser(SourceProductionContext context, KnownTypeSymbols typeSymbols) { _context = context; - _typeData = typeData; + _typeSymbols = typeSymbols; } public SourceGenerationSpec? GetSourceGenerationSpec(ImmutableArray operations) { - if (_typeData.SymbolForIConfiguration is null || _typeData.SymbolForIServiceCollection is null) + if (_typeSymbols.IConfiguration is null || _typeSymbols.IServiceCollection is null) { return null; } @@ -89,7 +90,7 @@ private void ProcessBindCall(BinderInvocationOperation binderOperation) // We're looking for IConfiguration.Bind(object). if (operation is IInvocationOperation { Arguments: { Length: 2 } arguments } && operation.TargetMethod.IsExtensionMethod && - TypesAreEqual(_typeData.SymbolForIConfiguration, arguments[0].Parameter.Type) && + TypesAreEqual(_typeSymbols.IConfiguration, arguments[0].Parameter.Type) && arguments[1].Parameter.Type.SpecialType == SpecialType.System_Object) { IConversionOperation argument = arguments[1].Value as IConversionOperation; @@ -129,7 +130,7 @@ private void ProcessGetCall(BinderInvocationOperation binderOperation) if (operation is IInvocationOperation { Arguments.Length: 1 } invocationOperation && invocationOperation.TargetMethod.IsExtensionMethod && invocationOperation.TargetMethod.IsGenericMethod && - TypesAreEqual(_typeData.SymbolForIConfiguration, invocationOperation.TargetMethod.Parameters[0].Type)) + TypesAreEqual(_typeSymbols.IConfiguration, invocationOperation.TargetMethod.Parameters[0].Type)) { ITypeSymbol? type = invocationOperation.TargetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None); if (type is not INamedTypeSymbol { } namedType || @@ -151,8 +152,8 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) if (operation is IInvocationOperation { Arguments.Length: 2 } invocationOperation && invocationOperation.TargetMethod.IsExtensionMethod && invocationOperation.TargetMethod.IsGenericMethod && - TypesAreEqual(_typeData.SymbolForIServiceCollection, invocationOperation.TargetMethod.Parameters[0].Type) && - TypesAreEqual(_typeData.SymbolForIConfiguration, invocationOperation.TargetMethod.Parameters[1].Type)) + TypesAreEqual(_typeSymbols.IServiceCollection, invocationOperation.TargetMethod.Parameters[0].Type) && + TypesAreEqual(_typeSymbols.IConfiguration, invocationOperation.TargetMethod.Parameters[1].Type)) { ITypeSymbol? type = invocationOperation.TargetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None); if (type is not INamedTypeSymbol { } namedType || @@ -189,11 +190,7 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) return spec; } - if (type.SpecialType == SpecialType.System_Object) - { - return CacheSpec(new TypeSpec(type) { Location = location, SpecKind = TypeSpecKind.System_Object }); - } - else if (type is INamedTypeSymbol { IsGenericType: true } genericType && + if (type is INamedTypeSymbol { IsGenericType: true } genericType && genericType.ConstructUnboundGenericType() is INamedTypeSymbol { } unboundGeneric && unboundGeneric.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) { @@ -201,40 +198,49 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) ? CacheSpec(new NullableSpec(type) { Location = location, UnderlyingType = underlyingType }) : null; } - else if (type.SpecialType != SpecialType.None) + else if (IsSupportedArrayType(type, location, out ITypeSymbol? elementType)) { - return CacheSpec(new TypeSpec(type) { Location = location }); + if (elementType.SpecialType is SpecialType.System_Byte) + { + return CacheSpec(new ParsableFromStringTypeSpec(type) { Location = location, StringParseableTypeKind = StringParsableTypeKind.ByteArray }); + } + + spec = CreateArraySpec((type as IArrayTypeSymbol)!, location); + if (spec is null) + { + return null; + } + + _typesForBindCoreMethodGen.Add(spec); + return CacheSpec(spec); } - else if (IsEnum(type)) + else if (IsParsableFromString(type, out StringParsableTypeKind specialTypeKind)) { - return CacheSpec(new TypeSpec(type) { Location = location, SpecKind = TypeSpecKind.Enum }); + return CacheSpec( + new ParsableFromStringTypeSpec(type) + { + Location = location, + StringParseableTypeKind = specialTypeKind + }); } - else if (type is IArrayTypeSymbol { } arrayType) + else if (IsCollection(type)) { - spec = CreateArraySpec(arrayType, location); + spec = CreateCollectionSpec((INamedTypeSymbol)type, location); if (spec is null) { return null; } - if (spec.SpecKind != TypeSpecKind.ByteArray) - { - Debug.Assert(spec.SpecKind is TypeSpecKind.Array); - _typesForBindCoreMethodGen.Add(spec); - } - + _typesForBindCoreMethodGen.Add(spec); return CacheSpec(spec); } - else if (TypesAreEqual(type, _typeData.SymbolForIConfigurationSection)) + else if (TypesAreEqual(type, _typeSymbols.IConfigurationSection)) { - return CacheSpec(new TypeSpec(type) { Location = location, SpecKind = TypeSpecKind.IConfigurationSection }); + return CacheSpec(new ConfigurationSectionTypeSpec(type) { Location = location }); } else if (type is INamedTypeSymbol namedType) { - spec = IsCollection(namedType) - ? CreateCollectionSpec(namedType, location) - : CreateObjectSpec(namedType, location); - + spec = CreateObjectSpec(namedType, location); if (spec is null) { return null; @@ -260,6 +266,104 @@ T CacheSpec(T? s) where T : TypeSpec } } + private bool IsParsableFromString(ITypeSymbol type, out StringParsableTypeKind typeKind) + { + if (type is not INamedTypeSymbol namedType) + { + typeKind = StringParsableTypeKind.None; + return false; + } + + if (IsEnum(namedType)) + { + typeKind = StringParsableTypeKind.Enum; + return true; + } + + SpecialType specialType = namedType.SpecialType; + + switch (specialType) + { + case SpecialType.System_String: + case SpecialType.System_Object: + { + typeKind = StringParsableTypeKind.ConfigValue; + return true; + } + case SpecialType.System_Boolean: + case SpecialType.System_Char: + { + typeKind = StringParsableTypeKind.Parse; + return true; + } + case SpecialType.System_Single: + case SpecialType.System_Double: + case SpecialType.System_Decimal: + { + typeKind = StringParsableTypeKind.Float; + return true; + } + case SpecialType.System_Byte: + case SpecialType.System_Int16: + case SpecialType.System_Int32: + case SpecialType.System_Int64: + case SpecialType.System_SByte: + case SpecialType.System_UInt16: + case SpecialType.System_UInt32: + case SpecialType.System_UInt64: + { + typeKind = StringParsableTypeKind.Integer; + return true; + } + case SpecialType.System_DateTime: + { + typeKind = StringParsableTypeKind.ParseInvariant; + return true; + } + case SpecialType.None: + { + if (TypesAreEqual(type, _typeSymbols.CultureInfo)) + { + typeKind = StringParsableTypeKind.CultureInfo; + } + else if (TypesAreEqual(type, _typeSymbols.DateTimeOffset) || + TypesAreEqual(type, _typeSymbols.DateOnly) || + TypesAreEqual(type, _typeSymbols.Guid) || + TypesAreEqual(type, _typeSymbols.TimeOnly) || + TypesAreEqual(type, _typeSymbols.TimeSpan)) + { + typeKind = StringParsableTypeKind.ParseInvariant; + } + else if (TypesAreEqual(type, _typeSymbols.Int128) || + TypesAreEqual(type, _typeSymbols.Half) || + TypesAreEqual(type, _typeSymbols.UInt128)) + { + typeKind = StringParsableTypeKind.ParseInvariant; + } + else if (TypesAreEqual(type, _typeSymbols.Uri)) + { + typeKind = StringParsableTypeKind.Uri; + } + else if (TypesAreEqual(type, _typeSymbols.Version)) + { + typeKind = StringParsableTypeKind.Parse; + } + else + { + typeKind = StringParsableTypeKind.None; + return false; + } + + return true; + } + default: + { + typeKind = StringParsableTypeKind.None; + return false; + } + } + } + private bool TryGetTypeSpec(ITypeSymbol type, string unsupportedReason, out TypeSpec? spec) { spec = GetOrCreateTypeSpec(type); @@ -273,41 +377,43 @@ private bool TryGetTypeSpec(ITypeSymbol type, string unsupportedReason, out Type return true; } - private EnumerableSpec? CreateArraySpec(IArrayTypeSymbol arrayType, Location? location) + private ArraySpec? CreateArraySpec(IArrayTypeSymbol arrayType, Location? location) { - if (arrayType.Rank > 1) + if (!TryGetTypeSpec(arrayType.ElementType, NotSupportedReason.ElementTypeNotSupported, out TypeSpec elementSpec)) { - ReportUnsupportedType(arrayType, NotSupportedReason.MultiDimArraysNotSupported, location); return null; } - if (!TryGetTypeSpec(arrayType.ElementType, NotSupportedReason.ElementTypeNotSupported, out TypeSpec? elementSpec)) + // We want a Bind method for List as a temp holder for the array values. + EnumerableSpec? listSpec = ConstructAndCacheGenericTypeForBind(_typeSymbols.List, arrayType.ElementType) as EnumerableSpec; + // We know the element type is supported. + Debug.Assert(listSpec != null); + + return new ArraySpec(arrayType) { - return null; - } + Location = location, + ElementType = elementSpec, + ConcreteType = listSpec, + }; + } - EnumerableSpec spec; - if (elementSpec.SpecialType is SpecialType.System_Byte) + private bool IsSupportedArrayType(ITypeSymbol type, Location? location, [NotNullWhen(true)] out ITypeSymbol? elementType) + { + if (type is not IArrayTypeSymbol arrayType) { - spec = new EnumerableSpec(arrayType) { Location = location, SpecKind = TypeSpecKind.ByteArray, ElementType = elementSpec }; + elementType = null; + return false; } - else - { - // We want a Bind method for List as a temp holder for the array values. - EnumerableSpec? listSpec = ConstructAndCacheGenericTypeForBind(_typeData.SymbolForList, arrayType.ElementType) as EnumerableSpec; - // We know the element type is supported. - Debug.Assert(listSpec != null); - spec = new EnumerableSpec(arrayType) - { - Location = location, - SpecKind = TypeSpecKind.Array, - ElementType = elementSpec, - ConcreteType = listSpec, - }; + if (arrayType.Rank > 1) + { + ReportUnsupportedType(arrayType, NotSupportedReason.MultiDimArraysNotSupported, location); + elementType = null; + return false; } - return spec; + elementType = arrayType.ElementType; + return true; } private CollectionSpec? CreateCollectionSpec(INamedTypeSymbol type, Location? location) @@ -332,17 +438,17 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc return null; } - if (keySpec.SpecKind != TypeSpecKind.StringBasedParse) + if (keySpec.SpecKind != TypeSpecKind.ParsableFromString) { ReportUnsupportedType(type, NotSupportedReason.DictionaryKeyNotSupported, location); return null; } DictionarySpec? concreteType = null; - if (IsInterfaceMatch(type, _typeData.SymbolForGenericIDictionary) || IsInterfaceMatch(type, _typeData.SymbolForIDictionary)) + if (IsInterfaceMatch(type, _typeSymbols.GenericIDictionary) || IsInterfaceMatch(type, _typeSymbols.IDictionary)) { // We know the key and element types are supported. - concreteType = ConstructAndCacheGenericTypeForBind(_typeData.SymbolForDictionary, keyType, elementType) as DictionarySpec; + concreteType = ConstructAndCacheGenericTypeForBind(_typeSymbols.Dictionary, keyType, elementType) as DictionarySpec; Debug.Assert(concreteType != null); } else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType, keyType)) @@ -354,7 +460,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc return new DictionarySpec(type) { Location = location, - KeyType = keySpec, + KeyType = (ParsableFromStringTypeSpec)keySpec, ElementType = elementSpec, ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor, ConcreteType = concreteType @@ -375,14 +481,14 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc } EnumerableSpec? concreteType = null; - if (IsInterfaceMatch(type, _typeData.SymbolForISet)) + if (IsInterfaceMatch(type, _typeSymbols.ISet)) { - concreteType = ConstructAndCacheGenericTypeForBind(_typeData.SymbolForHashSet, elementType) as EnumerableSpec; + concreteType = ConstructAndCacheGenericTypeForBind(_typeSymbols.HashSet, elementType) as EnumerableSpec; } - else if (IsInterfaceMatch(type, _typeData.SymbolForICollection) || - IsInterfaceMatch(type, _typeData.SymbolForGenericIList)) + else if (IsInterfaceMatch(type, _typeSymbols.ICollection) || + IsInterfaceMatch(type, _typeSymbols.GenericIList)) { - concreteType = ConstructAndCacheGenericTypeForBind(_typeData.SymbolForList, elementType) as EnumerableSpec; + concreteType = ConstructAndCacheGenericTypeForBind(_typeSymbols.List, elementType) as EnumerableSpec; } else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType)) { @@ -414,7 +520,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc _createdSpecs.Add(type, objectSpec); INamedTypeSymbol current = type; - while (current != null) + while (current is not null) { foreach (ISymbol member in current.GetMembers()) { @@ -431,7 +537,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc } else { - AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => TypesAreEqual(a.AttributeClass, _typeData.SymbolForConfigurationKeyNameAttribute)); + AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => TypesAreEqual(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute)); string? configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName; PropertySpec spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; @@ -451,7 +557,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc private bool IsCandidateEnumerable(INamedTypeSymbol type, out ITypeSymbol? elementType) { - INamedTypeSymbol? @interface = GetInterface(type, _typeData.SymbolForICollection); + INamedTypeSymbol? @interface = GetInterface(type, _typeSymbols.ICollection); if (@interface is not null) { @@ -465,7 +571,7 @@ private bool IsCandidateEnumerable(INamedTypeSymbol type, out ITypeSymbol? eleme private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyType, out ITypeSymbol? elementType) { - INamedTypeSymbol? @interface = GetInterface(type, _typeData.SymbolForGenericIDictionary); + INamedTypeSymbol? @interface = GetInterface(type, _typeSymbols.GenericIDictionary); if (@interface is not null) { keyType = @interface.TypeArguments[0]; @@ -473,10 +579,10 @@ private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyTy return true; } - if (IsInterfaceMatch(type, _typeData.SymbolForIDictionary)) + if (IsInterfaceMatch(type, _typeSymbols.IDictionary)) { - keyType = _typeData.SymbolForString; - elementType = _typeData.SymbolForString; + keyType = _typeSymbols.String; + elementType = _typeSymbols.String; return true; } @@ -485,8 +591,8 @@ private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyTy return false; } - private bool IsCollection(INamedTypeSymbol type) => - GetInterface(type, _typeData.SymbolForIEnumerable) is not null; + private bool IsCollection(ITypeSymbol type) => + type is INamedTypeSymbol namedType && GetInterface(namedType, _typeSymbols.IEnumerable) is not null; private static INamedTypeSymbol? GetInterface(INamedTypeSymbol type, INamedTypeSymbol @interface) { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs index 11a4269ae22ec..82f645a0940b3 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs @@ -57,7 +57,7 @@ private static void Execute(CompilationData compilationData, ImmutableArray= LanguageVersion.CSharp11; if (LanguageVersionIsSupported) { - TypeData = new KnownTypeData(compilation); + TypeSymbols = new KnownTypeSymbols(compilation); } } } - private sealed record KnownTypeData + private sealed record KnownTypeSymbols { - public INamedTypeSymbol SymbolForGenericIList { get; } - public INamedTypeSymbol SymbolForICollection { get; } - public INamedTypeSymbol SymbolForIEnumerable { get; } - public INamedTypeSymbol SymbolForString { get; } - - public INamedTypeSymbol? SymbolForConfigurationKeyNameAttribute { get; } - public INamedTypeSymbol? SymbolForDictionary { get; } - public INamedTypeSymbol? SymbolForGenericIDictionary { get; } - public INamedTypeSymbol? SymbolForHashSet { get; } - public INamedTypeSymbol? SymbolForIConfiguration { get; } - public INamedTypeSymbol? SymbolForIConfigurationSection { get; } - public INamedTypeSymbol? SymbolForIDictionary { get; } - public INamedTypeSymbol? SymbolForIServiceCollection { get; } - public INamedTypeSymbol? SymbolForISet { get; } - public INamedTypeSymbol? SymbolForList { get; } - - public KnownTypeData(CSharpCompilation compilation) + public INamedTypeSymbol GenericIList { get; } + public INamedTypeSymbol ICollection { get; } + public INamedTypeSymbol IEnumerable { get; } + public INamedTypeSymbol String { get; } + + public INamedTypeSymbol? CultureInfo { get; } + public INamedTypeSymbol? DateOnly { get; } + public INamedTypeSymbol? DateTimeOffset { get; } + public INamedTypeSymbol? Guid { get; } + public INamedTypeSymbol? Half { get; } + public INamedTypeSymbol? Int128 { get; } + public INamedTypeSymbol? TimeOnly { get; } + public INamedTypeSymbol? TimeSpan { get; } + public INamedTypeSymbol? UInt128 { get; } + public INamedTypeSymbol? Uri { get; } + public INamedTypeSymbol? Version { get; } + + public INamedTypeSymbol? ConfigurationKeyNameAttribute { get; } + public INamedTypeSymbol? Dictionary { get; } + public INamedTypeSymbol? GenericIDictionary { get; } + public INamedTypeSymbol? HashSet { get; } + public INamedTypeSymbol? IConfiguration { get; } + public INamedTypeSymbol? IConfigurationSection { get; } + public INamedTypeSymbol? IDictionary { get; } + public INamedTypeSymbol? IServiceCollection { get; } + public INamedTypeSymbol? ISet { get; } + public INamedTypeSymbol? List { get; } + + public KnownTypeSymbols(CSharpCompilation compilation) { - SymbolForIEnumerable = compilation.GetSpecialType(SpecialType.System_Collections_IEnumerable); - SymbolForConfigurationKeyNameAttribute = compilation.GetBestTypeByMetadataName(TypeFullName.ConfigurationKeyNameAttribute); - SymbolForIConfiguration = compilation.GetBestTypeByMetadataName(TypeFullName.IConfiguration); - SymbolForIConfigurationSection = compilation.GetBestTypeByMetadataName(TypeFullName.IConfigurationSection); - SymbolForIServiceCollection = compilation.GetBestTypeByMetadataName(TypeFullName.IServiceCollection); - SymbolForString = compilation.GetSpecialType(SpecialType.System_String); - - // Collections - SymbolForIDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.IDictionary); - - // Use for type equivalency checks for unbounded generics - SymbolForICollection = compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T).ConstructUnboundGenericType(); - SymbolForGenericIDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.GenericIDictionary)?.ConstructUnboundGenericType(); - SymbolForGenericIList = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IList_T).ConstructUnboundGenericType(); - SymbolForISet = compilation.GetBestTypeByMetadataName(TypeFullName.ISet)?.ConstructUnboundGenericType(); - - // Used to construct concrete types at runtime; cannot also be constructed - SymbolForDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.Dictionary); - SymbolForHashSet = compilation.GetBestTypeByMetadataName(TypeFullName.HashSet); - SymbolForList = compilation.GetBestTypeByMetadataName(TypeFullName.List); + // Primitives + CultureInfo = compilation.GetBestTypeByMetadataName(TypeFullName.CultureInfo); + DateOnly = compilation.GetBestTypeByMetadataName(TypeFullName.DateOnly); + DateTimeOffset = compilation.GetBestTypeByMetadataName(TypeFullName.DateTimeOffset); + Guid = compilation.GetBestTypeByMetadataName(TypeFullName.Guid); + Half = compilation.GetBestTypeByMetadataName(TypeFullName.Half); + Int128 = compilation.GetBestTypeByMetadataName(TypeFullName.Int128); + TimeOnly = compilation.GetBestTypeByMetadataName(TypeFullName.TimeOnly); + TimeSpan = compilation.GetBestTypeByMetadataName(TypeFullName.TimeSpan); + UInt128 = compilation.GetBestTypeByMetadataName(TypeFullName.UInt128); + Uri = compilation.GetBestTypeByMetadataName(TypeFullName.Uri); + Version = compilation.GetBestTypeByMetadataName(TypeFullName.Version); + + // Used to verify input configuation binding API calls. + ConfigurationKeyNameAttribute = compilation.GetBestTypeByMetadataName(TypeFullName.ConfigurationKeyNameAttribute); + IConfiguration = compilation.GetBestTypeByMetadataName(TypeFullName.IConfiguration); + IConfigurationSection = compilation.GetBestTypeByMetadataName(TypeFullName.IConfigurationSection); + IServiceCollection = compilation.GetBestTypeByMetadataName(TypeFullName.IServiceCollection); + + // Collections. + IEnumerable = compilation.GetSpecialType(SpecialType.System_Collections_IEnumerable); + IDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.IDictionary); + + // Used for type equivalency checks for unbounded generics. + ICollection = compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T).ConstructUnboundGenericType(); + GenericIDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.GenericIDictionary)?.ConstructUnboundGenericType(); + GenericIList = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IList_T).ConstructUnboundGenericType(); + ISet = compilation.GetBestTypeByMetadataName(TypeFullName.ISet)?.ConstructUnboundGenericType(); + + // Used to construct concrete types at runtime; cannot also be constructed. + Dictionary = compilation.GetBestTypeByMetadataName(TypeFullName.Dictionary); + HashSet = compilation.GetBestTypeByMetadataName(TypeFullName.HashSet); + List = compilation.GetBestTypeByMetadataName(TypeFullName.List); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationSectionTypeSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationSectionTypeSpec.cs new file mode 100644 index 0000000000000..533a98c0c0710 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationSectionTypeSpec.cs @@ -0,0 +1,13 @@ +// 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; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + internal sealed record ConfigurationSectionTypeSpec : TypeSpec + { + public ConfigurationSectionTypeSpec(ITypeSymbol type) : base(type) { } + public override TypeSpecKind SpecKind => TypeSpecKind.IConfigurationSection; + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConstructionStrategy.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConstructionStrategy.cs index e235b2cf48397..21db02547258a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConstructionStrategy.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConstructionStrategy.cs @@ -6,7 +6,6 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration internal enum ConstructionStrategy { NotApplicable = 0, - NotSupported = 1, - ParameterlessConstructor = 2, + ParameterlessConstructor = 1, } } 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 b37529ecbc87e..5005606f5bbdc 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 @@ -27,8 +27,10 @@ + + diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/NullableSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/NullableSpec.cs index 69ad69cdbfdd8..a75c2819548f2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/NullableSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/NullableSpec.cs @@ -9,7 +9,6 @@ internal sealed record NullableSpec : TypeSpec { public NullableSpec(ITypeSymbol type) : base(type) { } public override TypeSpecKind SpecKind => TypeSpecKind.Nullable; - public override ConstructionStrategy ConstructionStrategy => UnderlyingType.ConstructionStrategy; public required TypeSpec UnderlyingType { get; init; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ParsableFromStringTypeSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ParsableFromStringTypeSpec.cs new file mode 100644 index 0000000000000..a96e3c1c5494c --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ParsableFromStringTypeSpec.cs @@ -0,0 +1,14 @@ +// 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; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + internal sealed record ParsableFromStringTypeSpec : TypeSpec + { + public ParsableFromStringTypeSpec(ITypeSymbol type) : base(type) { } + public override TypeSpecKind SpecKind => TypeSpecKind.ParsableFromString; + public required StringParsableTypeKind StringParseableTypeKind { get; init; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpec.cs index 06ad7ceeb292d..d4da7d138863c 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpec.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { - internal record TypeSpec + internal abstract record TypeSpec { private static readonly SymbolDisplayFormat s_minimalDisplayFormat = new SymbolDisplayFormat( globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, @@ -18,7 +18,6 @@ public TypeSpec(ITypeSymbol type) FullyQualifiedDisplayString = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); MinimalDisplayString = type.ToDisplayString(s_minimalDisplayFormat); Namespace = type.ContainingNamespace?.ToDisplayString(); - SpecialType = type.SpecialType; IsValueType = type.IsValueType; } @@ -28,13 +27,9 @@ public TypeSpec(ITypeSymbol type) public string? Namespace { get; } - public SpecialType SpecialType { get; } - public bool IsValueType { get; } - public bool PassToBindCoreByRef => IsValueType || SpecKind == TypeSpecKind.Array; - - public virtual TypeSpecKind SpecKind { get; init; } + public abstract TypeSpecKind SpecKind { get; } public virtual ConstructionStrategy ConstructionStrategy { get; init; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpecKind.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpecKind.cs index 810a4aaae6eb0..dcfc27bc29718 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpecKind.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpecKind.cs @@ -5,15 +5,27 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { internal enum TypeSpecKind { - StringBasedParse = 0, - Enum = 1, + Unknown = 0, + ParsableFromString = 1, Object = 2, Array = 3, Enumerable = 4, Dictionary = 5, IConfigurationSection = 6, - System_Object = 7, - ByteArray = 8, - Nullable = 9, + Nullable = 7, + } + + internal enum StringParsableTypeKind + { + None = 0, + ConfigValue = 1, + Enum = 2, + ByteArray = 3, + Integer = 4, + Float = 5, + Parse = 6, + ParseInvariant = 7, + CultureInfo = 8, + Uri = 9, } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs index 9a90d0e0481b4..a1d1a72ffab20 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Test; namespace Microsoft.Extensions #if BUILDING_SOURCE_GENERATOR_TESTS @@ -11,14 +12,23 @@ namespace Microsoft.Extensions #endif .Configuration.Binder.Tests { - public static class TestHelpers + internal static class TestHelpers { - public static bool NotSourceGenMode + public const bool NotSourceGenMode #if BUILDING_SOURCE_GENERATOR_TESTS = false; #else = true; -#endif +#endif + + public static IConfiguration GetConfigurationFromJsonString(string json) + { + var builder = new ConfigurationBuilder(); + var configuration = builder + .AddJsonStream(TestStreamHelpers.StringToStream(json)) + .Build(); + return configuration; + } } #region // Shared test classes 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 650f4d004c71a..cc6eaf4fb38fc 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 @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Microsoft.Extensions.Configuration; using Xunit; @@ -566,5 +567,51 @@ public override string? TestVirtualSet public string? ExposeTestVirtualSet() => _testVirtualSet; } + + public class ClassWithDirectSelfReference + { + public string MyString { get; set; } + public ClassWithDirectSelfReference MyClass { get; set; } + } + + public class ClassWithIndirectSelfReference + { + public string MyString { get; set; } + public List MyList { get; set; } + } + + public record RecordWithPrimitives + { + public bool Prop0 { get; set; } + public byte Prop1 { get; set; } + public sbyte Prop2 { get; set; } + public char Prop3 { get; set; } + public double Prop4 { get; set; } + public string Prop5 { get; set; } + public int Prop6 { get; set; } + public short Prop8 { get; set; } + public long Prop9 { get; set; } + public float Prop10 { get; set; } + public ushort Prop13 { get; set; } + public uint Prop14 { get; set; } + public ulong Prop15 { get; set; } + public object Prop16 { get; set; } + public CultureInfo Prop17 { get; set; } + public DateTime Prop19 { get; set; } + public DateTimeOffset Prop20 { get; set; } + public decimal Prop21 { get; set; } + public TimeSpan Prop23 { get; set; } + public Guid Prop24 { get; set; } + public Uri Prop25 { get; set; } + public Version Prop26 { get; set; } + public DayOfWeek Prop27 { get; set; } +#if NETCOREAPP + public Int128 Prop7 { get; set; } + public Half Prop11 { get; set; } + public UInt128 Prop12 { get; set; } + public DateOnly Prop18 { get; set; } + public TimeOnly Prop22 { get; set; } +#endif + } } } 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 89af4c37e21b6..a2d3a8fdf9647 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Reflection; using Microsoft.Extensions.Configuration; @@ -424,7 +425,7 @@ public void ConsistentExceptionOnFailedBinding(Type type) getValueException.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync + [Fact] public void ExceptionOnFailedBindingIncludesPath() { const string IncorrectValue = "Invalid data"; @@ -1274,7 +1275,7 @@ public void CanBindByteArrayWhenValueIsNull() Assert.Null(options.MyByteArray); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync + [Fact] public void ExceptionWhenTryingToBindToByteArray() { var dic = new Dictionary @@ -1463,12 +1464,6 @@ public void RecursiveTypeGraphs_DirectRef() Assert.Null(deeplyNested.MyClass); } - public class ClassWithDirectSelfReference - { - public string MyString { get; set; } - public ClassWithDirectSelfReference MyClass { get; set; } - } - [Fact] public void RecursiveTypeGraphs_IndirectRef() { @@ -1498,10 +1493,100 @@ public void RecursiveTypeGraphs_IndirectRef() Assert.Null(deeplyNested.MyList); } - public class ClassWithIndirectSelfReference + [Fact] + public void TypeWithPrimitives_Pass() { - public string MyString { get; set; } - public List MyList { get; set; } + var data = @"{ + ""Prop0"": true, + ""Prop1"": 1, + ""Prop2"": 2, + ""Prop3"": ""C"", + ""Prop4"": 3.2, + ""Prop5"": ""Hello, world!"", + ""Prop6"": 4, + ""Prop8"": 9, + ""Prop9"": 7, + ""Prop10"": 2.3, + ""Prop13"": 5, + ""Prop14"": 10, + ""Prop15"": 11, + ""Prop16"": ""obj always parsed as string"", + ""Prop17"": ""yo-NG"", + ""Prop19"": ""2023-03-29T18:23:43.9977489+00:00"", + ""Prop20"": ""2023-03-29T18:21:22.8046981+00:00"", + ""Prop21"": 5.3, + ""Prop23"": ""10675199.02:48:05.4775807"", + ""Prop24"": ""e905a75b-d195-494d-8938-e55dcee44574"", + ""Prop25"": ""https://microsoft.com"", + ""Prop26"": ""4.3.2.1"", + }"; + + var configuration = TestHelpers.GetConfigurationFromJsonString(data); + var obj = configuration.Get(); + + Assert.True(obj.Prop0); + Assert.Equal(1, obj.Prop1); + Assert.Equal(2, obj.Prop2); + Assert.Equal('C', obj.Prop3); + Assert.Equal(3.2, obj.Prop4); + Assert.Equal("Hello, world!", obj.Prop5); + Assert.Equal(4, obj.Prop6); + Assert.Equal(9, obj.Prop8); + Assert.Equal(7, obj.Prop9); + Assert.Equal((float)2.3, obj.Prop10); + Assert.Equal(5, obj.Prop13); + Assert.Equal((uint)10, obj.Prop14); + Assert.Equal((ulong)11, obj.Prop15); + Assert.Equal("obj always parsed as string", obj.Prop16); + Assert.Equal(CultureInfo.GetCultureInfoByIetfLanguageTag("yo-NG"), obj.Prop17); + Assert.Equal(DateTime.Parse("2023-03-29T18:23:43.9977489+00:00", CultureInfo.InvariantCulture), obj.Prop19); + Assert.Equal(DateTimeOffset.Parse("2023-03-29T18:21:22.8046981+00:00", CultureInfo.InvariantCulture), obj.Prop20); + Assert.Equal((decimal)5.3, obj.Prop21); + Assert.Equal(TimeSpan.Parse("10675199.02:48:05.4775807", CultureInfo.InvariantCulture), obj.Prop23); + Assert.Equal(Guid.Parse("e905a75b-d195-494d-8938-e55dcee44574", CultureInfo.InvariantCulture), obj.Prop24); + Uri.TryCreate("https://microsoft.com", UriKind.RelativeOrAbsolute, out Uri? value); + Assert.Equal(value, obj.Prop25); + Assert.Equal(Version.Parse("4.3.2.1"), obj.Prop26); + Assert.Equal(CultureInfo.GetCultureInfoByIetfLanguageTag("yo-NG"), obj.Prop17); + +#if NETCOREAPP + data = @"{ + ""Prop7"": 9, + ""Prop11"": 65500, + ""Prop12"": 34, + ""Prop18"": ""2002-03-22"", + ""Prop22"": ""18:26:38.7327436"", + }"; + + configuration = TestHelpers.GetConfigurationFromJsonString(data); + configuration.Bind(obj); + + Assert.Equal((Int128)9, obj.Prop7); + Assert.Equal((Half)65500, obj.Prop11); + Assert.Equal((UInt128)34, obj.Prop12); + Assert.Equal(DateOnly.Parse("2002-03-22"), obj.Prop18); + Assert.Equal(TimeOnly.Parse("18:26:38.7327436"), obj.Prop22); +#endif + } + + [Theory] + [InlineData(0)] // bool + [InlineData(1)] // byte + [InlineData(4)] // double + [InlineData(17)] // CultureInfo + [InlineData(19)] // DateTime + [InlineData(26)] // Version + public void TypeWithPrimitives_Fail(int propIndex) + { + string prop = $"Prop{propIndex}"; + var data = $$""" + { + "{{prop}}": "Junk", + } + """; + IConfiguration configuration = TestHelpers.GetConfigurationFromJsonString(data); + var ex = Assert.Throws(configuration.Get); + Assert.Contains(prop, ex.ToString()); } } } 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 30c909d2d5e9d..495b8ecc9ad9d 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 @@ -9,7 +9,7 @@ internal static class GeneratedConfigurationBinder namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { using System; - using System.Linq; + using System.Globalization; using Microsoft.Extensions.Configuration; using System.Collections.Generic; @@ -22,13 +22,23 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - int element; foreach (IConfigurationSection section in configuration.GetChildren()) { - if (section.Value is string stringValue0) + if (HasValueOrChildren(section)) { - element = int.Parse(stringValue0); - obj.Add(element); + int element; + if (section.Value is string stringValue0) + { + try + { + element = int.Parse(stringValue0, NumberStyles.Integer, CultureInfo.InvariantCulture); + obj.Add(element); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{section.Path}' to type '{typeof(int)}'.", exception); + } + } } } } @@ -40,17 +50,14 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - string key; foreach (IConfigurationSection section in configuration.GetChildren()) { - if (section.Key is string stringValue1) + if (HasValueOrChildren(section)) { - key = stringValue1; - string element; - if (section.Value is string stringValue2) + string key = section.Key; + if (section.Value is string stringValue1) { - element = stringValue2; - obj[key] = element; + obj[key] = stringValue1; } } } @@ -67,23 +74,17 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - string key; foreach (IConfigurationSection section in configuration.GetChildren()) { - if (section.Key is string stringValue3) + if (HasValueOrChildren(section)) { - key = stringValue3; - if (obj.TryGetValue(key, out Program.MyClass2? element) && element is not null) - { - BindCore(section, ref element); - obj[key] = element; - } - else + string key = section.Key; + if (!(obj.TryGetValue(key, out Program.MyClass2? element) && element is not null)) { element = new Program.MyClass2(); - BindCore(section, ref element); - obj[key] = element; } + BindCore(section, ref element!); + obj[key] = element; } } } @@ -95,41 +96,48 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - if (configuration["MyString"] is string stringValue6) + if (configuration["MyString"] is string stringValue3) { - obj.MyString = stringValue6; + obj.MyString = stringValue3; } - if (configuration["MyInt"] is string stringValue7) + if (configuration["MyInt"] is string stringValue4) { - obj.MyInt = int.Parse(stringValue7); + try + { + obj.MyInt = int.Parse(stringValue4, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyInt").Path}' to type '{typeof(int)}'.", exception); + } } - IConfigurationSection section8 = configuration.GetSection("MyList"); - if (HasChildren(section8)) + IConfigurationSection section5 = configuration.GetSection("MyList"); + if (HasChildren(section5)) { - List temp9 = obj.MyList; - temp9 ??= new List(); - BindCore(section8, ref temp9); - obj.MyList = temp9; + List temp6 = obj.MyList; + temp6 ??= new List(); + BindCore(section5, ref temp6); + obj.MyList = temp6; } - IConfigurationSection section10 = configuration.GetSection("MyDictionary"); - if (HasChildren(section10)) + IConfigurationSection section7 = configuration.GetSection("MyDictionary"); + if (HasChildren(section7)) { - Dictionary temp11 = obj.MyDictionary; - temp11 ??= new Dictionary(); - BindCore(section10, ref temp11); - obj.MyDictionary = temp11; + Dictionary temp8 = obj.MyDictionary; + temp8 ??= new Dictionary(); + BindCore(section7, ref temp8); + obj.MyDictionary = temp8; } - IConfigurationSection section12 = configuration.GetSection("MyComplexDictionary"); - if (HasChildren(section12)) + IConfigurationSection section9 = configuration.GetSection("MyComplexDictionary"); + if (HasChildren(section9)) { - Dictionary temp13 = obj.MyComplexDictionary; - temp13 ??= new Dictionary(); - BindCore(section12, ref temp13); - obj.MyComplexDictionary = temp13; + Dictionary temp10 = obj.MyComplexDictionary; + temp10 ??= new Dictionary(); + BindCore(section9, ref temp10); + obj.MyComplexDictionary = temp10; } } 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 6f19224ea7b84..3f90f909cc428 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 @@ -30,7 +30,7 @@ internal static class GeneratedConfigurationBinder namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { using System; - using System.Linq; + using System.Globalization; using Microsoft.Extensions.Configuration; using System.Collections.Generic; @@ -43,13 +43,23 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - int element; foreach (IConfigurationSection section in configuration.GetChildren()) { - if (section.Value is string stringValue1) + if (HasValueOrChildren(section)) { - element = int.Parse(stringValue1); - obj.Add(element); + int element; + if (section.Value is string stringValue1) + { + try + { + element = int.Parse(stringValue1, NumberStyles.Integer, CultureInfo.InvariantCulture); + obj.Add(element); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{section.Path}' to type '{typeof(int)}'.", exception); + } + } } } } @@ -61,17 +71,14 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - string key; foreach (IConfigurationSection section in configuration.GetChildren()) { - if (section.Key is string stringValue2) + if (HasValueOrChildren(section)) { - key = stringValue2; - string element; - if (section.Value is string stringValue3) + string key = section.Key; + if (section.Value is string stringValue2) { - element = stringValue3; - obj[key] = element; + obj[key] = stringValue2; } } } @@ -84,32 +91,39 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - if (configuration["MyString"] is string stringValue4) + if (configuration["MyString"] is string stringValue3) { - obj.MyString = stringValue4; + obj.MyString = stringValue3; } - if (configuration["MyInt"] is string stringValue5) + if (configuration["MyInt"] is string stringValue4) { - obj.MyInt = int.Parse(stringValue5); + try + { + obj.MyInt = int.Parse(stringValue4, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyInt").Path}' to type '{typeof(int)}'.", exception); + } } - IConfigurationSection section6 = configuration.GetSection("MyList"); - if (HasChildren(section6)) + IConfigurationSection section5 = configuration.GetSection("MyList"); + if (HasChildren(section5)) { - List temp7 = obj.MyList; - temp7 ??= new List(); - BindCore(section6, ref temp7); - obj.MyList = temp7; + List temp6 = obj.MyList; + temp6 ??= new List(); + BindCore(section5, ref temp6); + obj.MyList = temp6; } - IConfigurationSection section8 = configuration.GetSection("MyDictionary"); - if (HasChildren(section8)) + IConfigurationSection section7 = configuration.GetSection("MyDictionary"); + if (HasChildren(section7)) { - Dictionary temp9 = obj.MyDictionary; - temp9 ??= new Dictionary(); - BindCore(section8, ref temp9); - obj.MyDictionary = temp9; + Dictionary temp8 = obj.MyDictionary; + temp8 ??= new Dictionary(); + BindCore(section7, ref temp8); + obj.MyDictionary = temp8; } } 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 32e497efd4df1..c60c671f7268a 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 @@ -29,7 +29,7 @@ internal static class GeneratedConfigurationBinder namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { using System; - using System.Linq; + using System.Globalization; using Microsoft.Extensions.Configuration; using System.Collections.Generic; @@ -42,13 +42,23 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - int element; foreach (IConfigurationSection section in configuration.GetChildren()) { - if (section.Value is string stringValue1) + if (HasValueOrChildren(section)) { - element = int.Parse(stringValue1); - obj.Add(element); + int element; + if (section.Value is string stringValue1) + { + try + { + element = int.Parse(stringValue1, NumberStyles.Integer, CultureInfo.InvariantCulture); + obj.Add(element); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{section.Path}' to type '{typeof(int)}'.", exception); + } + } } } } @@ -60,17 +70,14 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - string key; foreach (IConfigurationSection section in configuration.GetChildren()) { - if (section.Key is string stringValue2) + if (HasValueOrChildren(section)) { - key = stringValue2; - string element; - if (section.Value is string stringValue3) + string key = section.Key; + if (section.Value is string stringValue2) { - element = stringValue3; - obj[key] = element; + obj[key] = stringValue2; } } } @@ -83,32 +90,39 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - if (configuration["MyString"] is string stringValue4) + if (configuration["MyString"] is string stringValue3) { - obj.MyString = stringValue4; + obj.MyString = stringValue3; } - if (configuration["MyInt"] is string stringValue5) + if (configuration["MyInt"] is string stringValue4) { - obj.MyInt = int.Parse(stringValue5); + try + { + obj.MyInt = int.Parse(stringValue4, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyInt").Path}' to type '{typeof(int)}'.", exception); + } } - IConfigurationSection section6 = configuration.GetSection("MyList"); - if (HasChildren(section6)) + IConfigurationSection section5 = configuration.GetSection("MyList"); + if (HasChildren(section5)) { - List temp7 = obj.MyList; - temp7 ??= new List(); - BindCore(section6, ref temp7); - obj.MyList = temp7; + List temp6 = obj.MyList; + temp6 ??= new List(); + BindCore(section5, ref temp6); + obj.MyList = temp6; } - IConfigurationSection section8 = configuration.GetSection("MyDictionary"); - if (HasChildren(section8)) + IConfigurationSection section7 = configuration.GetSection("MyDictionary"); + if (HasChildren(section7)) { - Dictionary temp9 = obj.MyDictionary; - temp9 ??= new Dictionary(); - BindCore(section8, ref temp9); - obj.MyDictionary = temp9; + Dictionary temp8 = obj.MyDictionary; + temp8 ??= new Dictionary(); + BindCore(section7, ref temp8); + obj.MyDictionary = temp8; } } 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 new file mode 100644 index 0000000000000..d396919f46acf --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestPrimitivesGen.generated.txt @@ -0,0 +1,178 @@ +// +#nullable enable + +internal static class GeneratedConfigurationBinder +{ + public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration) + { + if (configuration is null) + { + throw new global::System.ArgumentNullException(nameof(configuration)); + } + + if (!global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.HasValueOrChildren(configuration)) + { + return default; + } + + if (typeof(T) == typeof(global::Program.MyClass)) + { + var obj = new global::Program.MyClass(); + global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.BindCore(configuration, ref obj); + return (T)(object)obj; + } + + throw new global::System.NotSupportedException($"Unable to bind to type '{typeof(T)}': 'Generator parser did not detect the type as input'"); + } +} + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + using System; + using System.Globalization; + using Microsoft.Extensions.Configuration; + + internal static class Helpers + { + public static void BindCore(IConfiguration configuration, ref Program.MyClass obj) + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (configuration["MyString"] is string stringValue1) + { + obj.MyString = stringValue1; + } + + if (configuration["MyInt128"] is string stringValue2) + { + try + { + obj.MyInt128 = Int128.Parse(stringValue2, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyInt128").Path}' to type '{typeof(Int128)}'.", exception); + } + } + + if (configuration["MyInt"] is string stringValue3) + { + try + { + obj.MyInt = int.Parse(stringValue3, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyInt").Path}' to type '{typeof(int)}'.", exception); + } + } + + if (configuration["MyUInt128"] is string stringValue4) + { + try + { + obj.MyUInt128 = UInt128.Parse(stringValue4, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyUInt128").Path}' to type '{typeof(UInt128)}'.", exception); + } + } + + if (configuration["MyLong"] is string stringValue5) + { + try + { + obj.MyLong = long.Parse(stringValue5, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyLong").Path}' to type '{typeof(long)}'.", exception); + } + } + + if (configuration["MyUri"] is string stringValue6) + { + Uri.TryCreate(stringValue6, UriKind.RelativeOrAbsolute, out Uri? temp7); + if (temp7 is not null) + { + obj.MyUri = temp7; + } + } + + if (configuration["MyCultureInfo"] is string stringValue8) + { + try + { + obj.MyCultureInfo = CultureInfo.GetCultureInfoByIetfLanguageTag(stringValue8); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyCultureInfo").Path}' to type '{typeof(CultureInfo)}'.", exception); + } + } + + if (configuration["MyHalf"] is string stringValue9) + { + try + { + obj.MyHalf = Half.Parse(stringValue9, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyHalf").Path}' to type '{typeof(Half)}'.", exception); + } + } + + if (configuration["MyBool"] is string stringValue10) + { + try + { + obj.MyBool = bool.Parse(stringValue10); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyBool").Path}' to type '{typeof(bool)}'.", exception); + } + } + + if (configuration["MyObject"] is string stringValue11) + { + obj.MyObject = stringValue11; + } + + if (configuration["MyByteArray"] is string stringValue12) + { + try + { + obj.MyByteArray = Convert.FromBase64String(stringValue12); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{configuration.GetSection("MyByteArray").Path}' to type '{typeof(byte[])}'.", exception); + } + } + } + + public static bool HasValueOrChildren(IConfiguration configuration) + { + if ((configuration as IConfigurationSection)?.Value is not null) + { + return true; + } + return HasChildren(configuration); + } + + public static bool HasChildren(IConfiguration configuration) + { + foreach (IConfigurationSection section in configuration.GetChildren()) + { + return true; + } + return false; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs index c0832f3010727..9c8854433d73a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs @@ -114,6 +114,45 @@ public class MyClass await VerifyAgainstBaselineUsingFile("TestConfigureCallGen.generated.txt", testSourceCode); } + [Fact] + public async Task TestBaseline_TestPrimitivesGen() + { + string testSourceCode = """ + using System; + using System.Collections.Generic; + using System.Globalization; + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfigurationRoot config = configurationBuilder.Build(); + + MyClass options = config.Get(); + } + + public class MyClass + { + public string MyString { get; set; } + public Int128 MyInt128 { get; set; } + public int MyInt { get; set; } + public UInt128 MyUInt128 { get; set; } + public long MyLong { get; set; } + public Uri MyUri { get; set; } + public CultureInfo MyCultureInfo { get; set; } + public Half MyHalf { get; set; } + public bool MyBool { get; set; } + public object MyObject { get; set; } + public byte[] MyByteArray { get; set; } + } + } + """; + + await VerifyAgainstBaselineUsingFile("TestPrimitivesGen.generated.txt", testSourceCode); + } + [Fact] public async Task LangVersionMustBeCharp11OrHigher() { @@ -140,6 +179,11 @@ private async Task VerifyAgainstBaselineUsingFile( Assert.Empty(d); Assert.Single(r); + if (!RoslynTestUtils.CompareLines(expectedLines, r[0].SourceText, out _)) + { + Console.WriteLine(r[0].SourceText); + } + Assert.True(RoslynTestUtils.CompareLines(expectedLines, r[0].SourceText, out string errorMessage), errorMessage); } @@ -151,11 +195,13 @@ await RoslynTestUtils.RunGenerator( new ConfigurationBindingSourceGenerator(), new[] { typeof(ConfigurationBinder).Assembly, + typeof(CultureInfo).Assembly, typeof(IConfiguration).Assembly, typeof(IServiceCollection).Assembly, typeof(IDictionary).Assembly, typeof(ServiceCollection).Assembly, typeof(OptionsConfigurationServiceCollectionExtensions).Assembly, + typeof(Uri).Assembly, }, new[] { testSourceCode }, langVersion: langVersion).ConfigureAwait(false);