From 0f222cd3c962cb5b0bf66ae4631424aa22d7921a Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Wed, 29 Mar 2023 18:18:14 -0700 Subject: [PATCH 1/2] Implement proper parsing for primitives in config binding gen --- .../gen/CollectionSpec.cs | 11 +- ...igurationBindingSourceGenerator.Emitter.cs | 321 +++++++++++------- ...igurationBindingSourceGenerator.Helpers.cs | 57 +++- ...figurationBindingSourceGenerator.Parser.cs | 252 ++++++++++---- .../ConfigurationBindingSourceGenerator.cs | 107 +++--- .../gen/ConfigurationSectionTypeSpec.cs | 13 + .../gen/ConstructionStrategy.cs | 3 +- ...nfiguration.Binder.SourceGeneration.csproj | 2 + .../gen/NullableSpec.cs | 1 - .../gen/ParsableFromStringTypeSpec.cs | 14 + .../gen/TypeSpec.cs | 9 +- .../gen/TypeSpecKind.cs | 22 +- .../ConfigurationBinderTests.Helpers.cs | 16 +- .../ConfigurationBinderTests.TestClasses.cs | 47 +++ .../tests/Common/ConfigurationBinderTests.cs | 107 +++++- .../Baselines/TestBindCallGen.generated.txt | 98 +++--- .../TestConfigureCallGen.generated.txt | 70 ++-- .../Baselines/TestGetCallGen.generated.txt | 70 ++-- .../Baselines/TestPrimitivesGen.generated.txt | 178 ++++++++++ ...nfingurationBindingSourceGeneratorTests.cs | 46 +++ 20 files changed, 1064 insertions(+), 380 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationSectionTypeSpec.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ParsableFromStringTypeSpec.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestPrimitivesGen.generated.txt diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs index 31058c49a009f9..f86f66803be639 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 046b518f70a45d..4e33574162cab0 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 2394107a2f6d65..d0ea5e0576cbec 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 26ed3669ccdb7f..6f98c6a3b41d7b 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 11a4269ae22eca..82f645a0940b36 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 00000000000000..533a98c0c07100 --- /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 e235b2cf483974..21db02547258ac 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 b37529ecbc87e7..5005606f5bbdca 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 69ad69cdbfdd8b..a75c2819548f20 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 00000000000000..a96e3c1c5494cc --- /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 06ad7ceeb292d5..d4da7d138863cb 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 810a4aaae6eb05..dcfc27bc297183 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 9a90d0e0481b4e..a1d1a72ffab20f 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 650f4d004c71af..cc6eaf4fb38fc7 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 89af4c37e21b60..a2d3a8fdf96474 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 30c909d2d5e9dd..495b8ecc9ad9d6 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 6f19224ea7b84d..3f90f909cc4288 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 32e497efd4df1d..c60c671f7268a0 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 00000000000000..d396919f46acf2 --- /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 c0832f30107279..9c8854433d73a3 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); From 3d50b854cef19d6ea81189b3c5b75cd6d5d413d4 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Tue, 11 Apr 2023 11:01:13 -0700 Subject: [PATCH 2/2] Address feedback --- ...igurationBindingSourceGenerator.Emitter.cs | 263 ++++----- ...igurationBindingSourceGenerator.Helpers.cs | 16 +- ...figurationBindingSourceGenerator.Parser.cs | 82 ++- .../ConfigurationBindingSourceGenerator.cs | 2 +- .../gen/ExceptionMessages.cs | 12 + .../gen/MethodSpecifier.cs | 21 + ...nfiguration.Binder.SourceGeneration.csproj | 2 + .../gen/ParsableFromStringTypeSpec.cs | 21 +- .../gen/SourceGenerationSpec.cs | 50 +- .../tests/Common/ConfigurationBinderTests.cs | 26 +- .../Baselines/TestBindCallGen.generated.txt | 32 +- .../TestConfigureCallGen.generated.txt | 32 +- .../Baselines/TestGetCallGen.generated.txt | 32 +- .../Baselines/TestPrimitivesGen.generated.txt | 549 ++++++++++++++---- ...nfingurationBindingSourceGeneratorTests.cs | 51 +- 15 files changed, 775 insertions(+), 416 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ExceptionMessages.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/MethodSpecifier.cs 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 4e33574162cab0..f9133090ed0c0a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs @@ -15,27 +15,18 @@ private sealed partial class Emitter { 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"; } 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 @@ -96,7 +87,7 @@ private void EmitConfigureMethod() EmitCheckForNullArgument_WithBlankLine(Identifier.configuration, useFullyQualifiedNames: true); - foreach (TypeSpec type in _generationSpec.Methods[MethodSpecifier.Configure]) + foreach (TypeSpec type in _generationSpec.RootConfigTypes[MethodSpecifier.Configure]) { string typeDisplayString = type.FullyQualifiedDisplayString; @@ -133,7 +124,7 @@ private void EmitGetMethod() EmitIConfigurationHasValueOrChildrenCheck(); - foreach (TypeSpec type in _generationSpec.Methods[MethodSpecifier.Get]) + foreach (TypeSpec type in _generationSpec.RootConfigTypes[MethodSpecifier.Get]) { string typeDisplayString = type.FullyQualifiedDisplayString; @@ -160,7 +151,7 @@ private void EmitBindMethods() _writer.WriteBlankLine(); } - foreach (TypeSpec type in _generationSpec.Methods[MethodSpecifier.Bind]) + foreach (TypeSpec type in _generationSpec.RootConfigTypes[MethodSpecifier.Bind]) { EmitBindMethod(type); _writer.WriteBlankLine(); @@ -207,7 +198,7 @@ private void EmitHelperUsingStatements() private void EmitBindCoreMethods() { - foreach (TypeSpec type in _generationSpec.Methods[MethodSpecifier.BindCore]) + foreach (TypeSpec type in _generationSpec.RootConfigTypes[MethodSpecifier.BindCore]) { EmitBindCoreMethod(type); _writer.WriteBlankLine(); @@ -228,17 +219,17 @@ private void EmitBindCoreImpl(TypeSpec type) { case TypeSpecKind.Array: { - EmitBindCoreImplForArray((type as ArraySpec)!); + EmitBindCoreImplForArray((ArraySpec)type); } break; case TypeSpecKind.Enumerable: { - EmitBindCoreImplForEnumerable((type as EnumerableSpec)!); + EmitBindCoreImplForEnumerable((EnumerableSpec)type); } break; case TypeSpecKind.Dictionary: { - EmitBindCoreImplForDictionary((type as DictionarySpec)!); + EmitBindCoreImplForDictionary((DictionarySpec)type); } break; case TypeSpecKind.IConfigurationSection: @@ -249,12 +240,12 @@ private void EmitBindCoreImpl(TypeSpec type) break; case TypeSpecKind.Object: { - EmitBindCoreImplForObject((type as ObjectSpec)!); + EmitBindCoreImplForObject((ObjectSpec)type); } break; case TypeSpecKind.Nullable: { - EmitBindCoreImpl((type as NullableSpec)!.UnderlyingType); + EmitBindCoreImpl(((NullableSpec)type).UnderlyingType); } break; default: @@ -265,8 +256,7 @@ private void EmitBindCoreImpl(TypeSpec type) private void EmitBindCoreImplForArray(ArraySpec type) { - EnumerableSpec concreteType = (type.ConcreteType as EnumerableSpec)!; - Debug.Assert(type.SpecKind == TypeSpecKind.Array && type.ConcreteType is not null); + EnumerableSpec concreteType = (EnumerableSpec)type.ConcreteType; EmitCheckForNullArgument_WithBlankLine_IfRequired(isValueType: false); @@ -274,7 +264,7 @@ private void EmitBindCoreImplForArray(ArraySpec type) string tempVarName = GetIncrementalVarName(Identifier.temp); EmitBindCoreCall(concreteType, tempVarName, Identifier.configuration, InitializationKind.Declaration); - // Resize array and copy fill with additional + // Resize array and copy additional elements. _writer.WriteBlock($$""" {{Identifier.Int32}} {{Identifier.originalCount}} = {{Identifier.obj}}.{{Identifier.Length}}; {{Identifier.Array}}.{{Identifier.Resize}}(ref {{Identifier.obj}}, {{Identifier.originalCount}} + {{tempVarName}}.{{Identifier.Count}}); @@ -296,7 +286,7 @@ private void EmitBindCoreImplForEnumerable(EnumerableSpec type) if (elementType.SpecKind is TypeSpecKind.ParsableFromString) { ParsableFromStringTypeSpec stringParsableType = (ParsableFromStringTypeSpec)elementType; - if (stringParsableType.StringParseableTypeKind is StringParsableTypeKind.ConfigValue) + if (stringParsableType.StringParsableTypeKind is StringParsableTypeKind.ConfigValue) { string tempVarName = GetIncrementalVarName(Identifier.stringValue); _writer.WriteBlockStart($"if ({Expression.sectionValue} is string {tempVarName})"); @@ -329,7 +319,7 @@ private void EmitBindCoreImplForDictionary(DictionarySpec type) // Parse key ParsableFromStringTypeSpec keyType = type.KeyType; - if (keyType.StringParseableTypeKind is StringParsableTypeKind.ConfigValue) + if (keyType.StringParsableTypeKind is StringParsableTypeKind.ConfigValue) { _writer.WriteLine($"{keyType.MinimalDisplayString} {Identifier.key} = {Expression.sectionKey};"); Emit_BindAndAddLogic_ForElement(); @@ -352,7 +342,7 @@ void Emit_BindAndAddLogic_ForElement() if (elementType.SpecKind == TypeSpecKind.ParsableFromString) { ParsableFromStringTypeSpec stringParsableType = (ParsableFromStringTypeSpec)elementType; - if (stringParsableType.StringParseableTypeKind is StringParsableTypeKind.ConfigValue) + if (stringParsableType.StringParsableTypeKind is StringParsableTypeKind.ConfigValue) { string tempVarName = GetIncrementalVarName(Identifier.stringValue); _writer.WriteBlockStart($"if ({Expression.sectionValue} is string {tempVarName})"); @@ -433,7 +423,7 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert if (canSet) { EmitBindLogicFromString( - (propertyType as ParsableFromStringTypeSpec)!, + (ParsableFromStringTypeSpec)propertyType, expressionForPropertyAccess, expressionForConfigValueIndexer, expressionForConfigValuePath: $@"{expressionForConfigSectionAccess}.{Identifier.Path}"); @@ -456,7 +446,7 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert break; case TypeSpecKind.Nullable: { - TypeSpec underlyingType = (propertyType as NullableSpec)!.UnderlyingType; + TypeSpec underlyingType = ((NullableSpec)propertyType).UnderlyingType; EmitBindCoreImplForProperty(property, underlyingType, parentType); } break; @@ -485,7 +475,7 @@ private void EmitBindLogicFromRootMethod(TypeSpec type, string expressionForMemb { EmitCastToIConfigurationSection(); } - EmitBindLogicFromString((type as ParsableFromStringTypeSpec)!, expressionForMemberAccess, Expression.sectionValue, Expression.sectionPath); + EmitBindLogicFromString((ParsableFromStringTypeSpec)type, expressionForMemberAccess, Expression.sectionValue, Expression.sectionPath); } else { @@ -599,113 +589,19 @@ private void EmitBindLogicFromString( string expressionForConfigValuePath, Action? writeOnSuccess = null) { - StringParsableTypeKind typeKind = type.StringParseableTypeKind; - string typeDisplayString = GetTypeDisplayString(type); + StringParsableTypeKind typeKind = type.StringParsableTypeKind; + Debug.Assert(typeKind is not StringParsableTypeKind.None); string stringValueVarName = GetIncrementalVarName(Identifier.stringValue); - string innerExceptionTypeDisplayString; - string cultureInfoTypeDisplayString; - string numberStylesTypeDisplayString; - if (_useFullyQualifiedNames) - { - innerExceptionTypeDisplayString = FullyQualifiedDisplayName.FormatException; - cultureInfoTypeDisplayString = FullyQualifiedDisplayName.CultureInfo; - numberStylesTypeDisplayString = FullyQualifiedDisplayName.NumberStyles; - } - else - { - innerExceptionTypeDisplayString = Identifier.Exception; - cultureInfoTypeDisplayString = Identifier.CultureInfo; - numberStylesTypeDisplayString = Identifier.NumberStyles; - } - _writer.WriteBlockStart($"if ({expressionForConfigStringValue} is string {stringValueVarName})"); - if (typeKind is StringParsableTypeKind.ConfigValue) - { - EmitAssignment(expressionForMemberAccess, stringValueVarName); - writeOnSuccess?.Invoke(); - _writer.WriteBlockEnd(); - return; - } - 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; - } - - // 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}}; - """); + string parsedValue = typeKind is StringParsableTypeKind.ConfigValue + ? stringValueVarName + : $"{GetHelperMethodDisplayString(type.ParseMethodName)}({stringValueVarName}, () => {expressionForConfigValuePath})"; + EmitAssignment(expressionForMemberAccess, parsedValue); writeOnSuccess?.Invoke(); - - _writer.WriteBlock($$""" - } - catch ({{innerExceptionTypeDisplayString}} {{Identifier.exception}}) - { - {{exceptionExpression}}; - } - """); - _writer.WriteBlockEnd(); return; @@ -775,6 +671,15 @@ private void EmitHelperMethods() { EmitHasChildrenMethod(); } + + if (_generationSpec.PrimitivesForHelperGen.Count > 0) + { + foreach (ParsableFromStringTypeSpec type in _generationSpec.PrimitivesForHelperGen) + { + _writer.WriteBlankLine(); + EmitPrimitiveParseMethod(type); + } + } } private void EmitHasValueOrChildrenMethod() @@ -805,6 +710,102 @@ private void EmitHasChildrenMethod() """); } + private void EmitPrimitiveParseMethod(ParsableFromStringTypeSpec type) + { + string innerExceptionTypeDisplayString; + string cultureInfoTypeDisplayString; + string numberStylesTypeDisplayString; + + if (_useFullyQualifiedNames) + { + innerExceptionTypeDisplayString = "global::System.Exception"; + cultureInfoTypeDisplayString = "global::System.Globalization.CultureInfo"; + numberStylesTypeDisplayString = "global::System.Globalization.NumberStyles"; + } + else + { + innerExceptionTypeDisplayString = "Exception"; + cultureInfoTypeDisplayString = "CultureInfo"; + numberStylesTypeDisplayString = "NumberStyles"; + } + + string invariantCultureExpression = $"{cultureInfoTypeDisplayString}.InvariantCulture"; + + string expressionForParsedValue; + StringParsableTypeKind typeKind = type.StringParsableTypeKind; + string typeDisplayString = type.MinimalDisplayString; + + switch (typeKind) + { + case StringParsableTypeKind.Enum: + { + expressionForParsedValue = $"({typeDisplayString}){Identifier.Enum}.{Identifier.Parse}(typeof({typeDisplayString}), {Identifier.stringValue}, ignoreCase: true)"; + } + break; + case StringParsableTypeKind.ByteArray: + { + expressionForParsedValue = $"Convert.FromBase64String({Identifier.stringValue})"; + } + break; + case StringParsableTypeKind.Integer: + { + expressionForParsedValue = $"{typeDisplayString}.{Identifier.Parse}({Identifier.stringValue}, {numberStylesTypeDisplayString}.Integer, {invariantCultureExpression})"; + } + break; + case StringParsableTypeKind.Float: + { + expressionForParsedValue = $"{typeDisplayString}.{Identifier.Parse}({Identifier.stringValue}, {numberStylesTypeDisplayString}.Float, {invariantCultureExpression})"; + } + break; + case StringParsableTypeKind.Parse: + { + expressionForParsedValue = $"{typeDisplayString}.{Identifier.Parse}({Identifier.stringValue})"; + } + break; + case StringParsableTypeKind.ParseInvariant: + { + expressionForParsedValue = $"{typeDisplayString}.{Identifier.Parse}({Identifier.stringValue}, {invariantCultureExpression})"; ; + } + break; + case StringParsableTypeKind.CultureInfo: + { + expressionForParsedValue = $"{cultureInfoTypeDisplayString}.GetCultureInfo({Identifier.stringValue})"; + } + break; + case StringParsableTypeKind.Uri: + { + expressionForParsedValue = $"new Uri({Identifier.stringValue}, UriKind.RelativeOrAbsolute)"; + } + break; + default: + { + Debug.Fail($"Invalid string parsable kind: {typeKind}"); + return; + } + } + + string exceptionTypeDisplayString = _useFullyQualifiedNames ? FullyQualifiedDisplayName.InvalidOperationException : Identifier.InvalidOperationException; + + _writer.WriteBlock($$""" + public static {{typeDisplayString}} {{type.ParseMethodName}}(string {{Identifier.stringValue}}, Func {{Identifier.getPath}}) + { + try + { + return {{expressionForParsedValue}}; + """); + + string exceptionArg1 = string.Format(ExceptionMessages.FailedBinding, $"{{{Identifier.getPath}()}}", $"{{typeof({typeDisplayString})}}"); + + _writer.WriteBlock($$""" + } + catch ({{innerExceptionTypeDisplayString}} {{Identifier.exception}}) + { + throw new {{exceptionTypeDisplayString}}($"{{exceptionArg1}}", {{Identifier.exception}}); + } + } + """); + } + private void EmitVarDeclaration(TypeSpec type, string varName) => _writer.WriteLine($"{type.MinimalDisplayString} {varName};"); private void EmitAssignment(string lhsSource, string rhsSource) => _writer.WriteLine($"{lhsSource} = {rhsSource};"); @@ -815,7 +816,7 @@ private void EmitCastToIConfigurationSection() string exceptionTypeDisplayString; if (_useFullyQualifiedNames) { - sectionTypeDisplayString = FullyQualifiedDisplayName.IConfigurationSection; + sectionTypeDisplayString = "global::Microsoft.Extensions.Configuration.IConfigurationSection"; exceptionTypeDisplayString = FullyQualifiedDisplayName.InvalidOperationException; } else @@ -833,7 +834,7 @@ private void EmitCastToIConfigurationSection() } private void Emit_NotSupportedException_UnableToBindType(string reason, string typeDisplayString = "{typeof(T)}") => - _writer.WriteLine(@$"throw new {FullyQualifiedDisplayName.NotSupportedException}($""{string.Format(ExceptionMessages.TypeNotSupported, typeDisplayString, reason)}"");"); + _writer.WriteLine(@$"throw new global::System.NotSupportedException($""{string.Format(ExceptionMessages.TypeNotSupported, typeDisplayString, reason)}"");"); private void EmitCheckForNullArgument_WithBlankLine_IfRequired(bool isValueType) { @@ -846,8 +847,8 @@ private void EmitCheckForNullArgument_WithBlankLine_IfRequired(bool isValueType) private void EmitCheckForNullArgument_WithBlankLine(string argName, bool useFullyQualifiedNames = false) { string exceptionTypeDisplayString = useFullyQualifiedNames - ? FullyQualifiedDisplayName.ArgumentNullException - : Identifier.ArgumentNullException; + ? "global::System.ArgumentNullException" + : "ArgumentNullException"; _writer.WriteBlock($$""" if ({{argName}} is null) 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 d0ea5e0576cbec..1dd35c4418d189 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs @@ -34,13 +34,6 @@ public sealed partial class ConfigurationBindingSourceGenerator defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); - // 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"; @@ -60,6 +53,7 @@ private static class Identifier public const string element = nameof(element); public const string enumValue = nameof(enumValue); public const string exception = nameof(exception); + public const string getPath = nameof(getPath); public const string key = nameof(key); public const string obj = nameof(obj); public const string originalCount = nameof(originalCount); @@ -71,7 +65,6 @@ private static class Identifier public const string Add = nameof(Add); public const string Any = nameof(Any); - public const string ArgumentNullException = nameof(ArgumentNullException); public const string Array = nameof(Array); public const string Bind = nameof(Bind); public const string BindCore = nameof(BindCore); @@ -82,12 +75,9 @@ private static class Identifier 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); @@ -96,20 +86,16 @@ 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); } 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 6f98c6a3b41d7b..077bf0f4f0b488 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs @@ -1,11 +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 System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Operations; @@ -15,8 +17,6 @@ public sealed partial class ConfigurationBindingSourceGenerator { private sealed class Parser { - private const string GlobalNameSpaceString = ""; - private readonly SourceProductionContext _context; private readonly KnownTypeSymbols _typeSymbols; @@ -28,6 +28,7 @@ private sealed class Parser private readonly HashSet _unsupportedTypes = new(SymbolEqualityComparer.Default); private readonly Dictionary _createdSpecs = new(SymbolEqualityComparer.Default); + private readonly HashSet _primitivesForHelperGen = new(); private readonly HashSet _namespaces = new() { "System", @@ -35,6 +36,8 @@ private sealed class Parser "Microsoft.Extensions.Configuration" }; + private MethodSpecifier _methodsToGen; + public Parser(SourceProductionContext context, KnownTypeSymbols typeSymbols) { _context = context; @@ -72,7 +75,7 @@ public Parser(SourceProductionContext context, KnownTypeSymbols typeSymbols) } } - Dictionary> methods = new() + Dictionary> rootConfigTypes = new() { [MethodSpecifier.Bind] = _typesForBindMethodGen, [MethodSpecifier.Get] = _typesForGetMethodGen, @@ -80,7 +83,7 @@ public Parser(SourceProductionContext context, KnownTypeSymbols typeSymbols) [MethodSpecifier.BindCore] = _typesForBindCoreMethodGen, }; - return new SourceGenerationSpec(methods, _namespaces); + return new SourceGenerationSpec(rootConfigTypes, _methodsToGen, _primitivesForHelperGen, _namespaces); } private void ProcessBindCall(BinderInvocationOperation binderOperation) @@ -105,7 +108,7 @@ private void ProcessBindCall(BinderInvocationOperation binderOperation) return; } - AddTargetConfigType(_typesForBindMethodGen, namedType, binderOperation.Location); + AddRootConfigType(MethodSpecifier.Bind, namedType, binderOperation.Location); static ITypeSymbol? ResolveType(IOperation argument) => argument switch @@ -140,7 +143,7 @@ private void ProcessGetCall(BinderInvocationOperation binderOperation) return; } - AddTargetConfigType(_typesForGetMethodGen, namedType, binderOperation.Location); + AddRootConfigType(MethodSpecifier.Get, namedType, binderOperation.Location); } } @@ -162,11 +165,11 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) return; } - AddTargetConfigType(_typesForConfigureMethodGen, namedType, binderOperation.Location); + AddRootConfigType(MethodSpecifier.Configure, namedType, binderOperation.Location); } } - private TypeSpec? AddTargetConfigType(HashSet specs, ITypeSymbol type, Location? location) + private TypeSpec? AddRootConfigType(MethodSpecifier method, ITypeSymbol type, Location? location) { if (type is not INamedTypeSymbol namedType || ContainsGenericParameters(namedType)) { @@ -174,10 +177,23 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) } TypeSpec? spec = GetOrCreateTypeSpec(namedType, location); - if (spec != null && - !specs.Contains(spec)) + HashSet types = method switch + { + MethodSpecifier.Configure => _typesForConfigureMethodGen, + MethodSpecifier.Get => _typesForGetMethodGen, + MethodSpecifier.Bind => _typesForBindMethodGen, + MethodSpecifier.BindCore => _typesForBindCoreMethodGen, + _ => throw new InvalidOperationException($"Invalid method for config binding method generation: {method}") + }; + + if (spec != null) { - specs.Add(spec); + types.Add(spec); + _methodsToGen |= method; + if (method is not MethodSpecifier.Bind) + { + _methodsToGen |= MethodSpecifier.HasValueOrChildren; + } } return spec; @@ -202,7 +218,7 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) { if (elementType.SpecialType is SpecialType.System_Byte) { - return CacheSpec(new ParsableFromStringTypeSpec(type) { Location = location, StringParseableTypeKind = StringParsableTypeKind.ByteArray }); + return CacheSpec(new ParsableFromStringTypeSpec(type) { Location = location, StringParsableTypeKind = StringParsableTypeKind.ByteArray }); } spec = CreateArraySpec((type as IArrayTypeSymbol)!, location); @@ -211,7 +227,7 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) return null; } - _typesForBindCoreMethodGen.Add(spec); + RegisterTypeForBindCoreMethodGen(MethodSpecifier.BindCore, spec); return CacheSpec(spec); } else if (IsParsableFromString(type, out StringParsableTypeKind specialTypeKind)) @@ -220,7 +236,7 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) new ParsableFromStringTypeSpec(type) { Location = location, - StringParseableTypeKind = specialTypeKind + StringParsableTypeKind = specialTypeKind }); } else if (IsCollection(type)) @@ -231,7 +247,7 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) return null; } - _typesForBindCoreMethodGen.Add(spec); + RegisterTypeForBindCoreMethodGen(MethodSpecifier.BindCore, spec); return CacheSpec(spec); } else if (TypesAreEqual(type, _typeSymbols.IConfigurationSection)) @@ -246,7 +262,7 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) return null; } - _typesForBindCoreMethodGen.Add(spec); + RegisterTypeForBindCoreMethodGen(MethodSpecifier.BindCore, spec); return CacheSpec(spec); } @@ -255,17 +271,35 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) T CacheSpec(T? s) where T : TypeSpec { + TypeSpecKind typeKind = s.SpecKind; + Debug.Assert(typeKind is not TypeSpecKind.Unknown); + string @namespace = s.Namespace; - if (@namespace != null && @namespace != GlobalNameSpaceString) + if (@namespace != null && @namespace != "") { _namespaces.Add(@namespace); } + if (typeKind is TypeSpecKind.ParsableFromString) + { + ParsableFromStringTypeSpec type = ((ParsableFromStringTypeSpec)(object)s); + if (type.StringParsableTypeKind is not StringParsableTypeKind.ConfigValue) + { + _primitivesForHelperGen.Add(type); + } + } + _createdSpecs[type] = s; return s; } } + private void RegisterTypeForBindCoreMethodGen(MethodSpecifier method, TypeSpec spec) + { + _typesForBindCoreMethodGen.Add(spec); + _methodsToGen |= method; + } + private bool IsParsableFromString(ITypeSymbol type, out StringParsableTypeKind typeKind) { if (type is not INamedTypeSymbol namedType) @@ -328,7 +362,6 @@ private bool IsParsableFromString(ITypeSymbol type, out StringParsableTypeKind t } else if (TypesAreEqual(type, _typeSymbols.DateTimeOffset) || TypesAreEqual(type, _typeSymbols.DateOnly) || - TypesAreEqual(type, _typeSymbols.Guid) || TypesAreEqual(type, _typeSymbols.TimeOnly) || TypesAreEqual(type, _typeSymbols.TimeSpan)) { @@ -344,7 +377,8 @@ private bool IsParsableFromString(ITypeSymbol type, out StringParsableTypeKind t { typeKind = StringParsableTypeKind.Uri; } - else if (TypesAreEqual(type, _typeSymbols.Version)) + else if (TypesAreEqual(type, _typeSymbols.Version) || + TypesAreEqual(type, _typeSymbols.Guid)) { typeKind = StringParsableTypeKind.Parse; } @@ -470,7 +504,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc private TypeSpec? ConstructAndCacheGenericTypeForBind(INamedTypeSymbol type, params ITypeSymbol[] parameters) { Debug.Assert(type.IsGenericType); - return AddTargetConfigType(_typesForBindMethodGen, type.Construct(parameters), location: null); + return AddRootConfigType(MethodSpecifier.Bind, type.Construct(parameters), location: null); } private EnumerableSpec? CreateEnumerableSpec(INamedTypeSymbol type, Location? location, ITypeSymbol elementType) @@ -545,6 +579,14 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc { objectSpec.Properties.Add(spec); } + + if (propertyTypeSpec.SpecKind is TypeSpecKind.Object or + TypeSpecKind.Array or + TypeSpecKind.Enumerable or + TypeSpecKind.Dictionary) + { + _methodsToGen |= MethodSpecifier.HasChildren; + } } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs index 82f645a0940b36..46b4ed1789b7e5 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs @@ -113,7 +113,7 @@ private sealed record KnownTypeSymbols public KnownTypeSymbols(CSharpCompilation compilation) { - // Primitives + // Primitives (needed because they are Microsoft.CodeAnalysis.SpecialType.None) CultureInfo = compilation.GetBestTypeByMetadataName(TypeFullName.CultureInfo); DateOnly = compilation.GetBestTypeByMetadataName(TypeFullName.DateOnly); DateTimeOffset = compilation.GetBestTypeByMetadataName(TypeFullName.DateTimeOffset); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ExceptionMessages.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ExceptionMessages.cs new file mode 100644 index 00000000000000..d31ed9a7836621 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ExceptionMessages.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + // Runtime exception messages; not localized so we keep them in source. + internal 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}'."; + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/MethodSpecifier.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/MethodSpecifier.cs new file mode 100644 index 00000000000000..ef928da735c533 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/MethodSpecifier.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + [Flags] + internal enum MethodSpecifier + { + None = 0x0, + // Root methods + Bind = 0x1, + Get = 0x2, + Configure = 0x4, + // Helper methods + BindCore = 0x8, + HasValueOrChildren = 0x10, + HasChildren = 0x20, + } +} 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 5005606f5bbdca..5b9340c1d16223 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj @@ -28,6 +28,8 @@ + + diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ParsableFromStringTypeSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ParsableFromStringTypeSpec.cs index a96e3c1c5494cc..96dd5f8072e896 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ParsableFromStringTypeSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ParsableFromStringTypeSpec.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.Diagnostics; using Microsoft.CodeAnalysis; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration @@ -8,7 +9,25 @@ 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; } + + public required StringParsableTypeKind StringParsableTypeKind { get; init; } + + private string? _parseMethodName; + public string ParseMethodName + { + get + { + Debug.Assert(StringParsableTypeKind is not StringParsableTypeKind.ConfigValue); + + _parseMethodName ??= StringParsableTypeKind is StringParsableTypeKind.ByteArray + ? "ParseByteArray" + // MinimalDisplayString.Length is certainly > 2. + : $"Parse{(char.ToUpper(MinimalDisplayString[0]) + MinimalDisplayString.Substring(1)).Replace(".", "")}"; + + return _parseMethodName; + } + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs index aeeb9e19a6daa6..f4964c5ba99932 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs @@ -1,59 +1,17 @@ // 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; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { internal sealed record SourceGenerationSpec( - Dictionary> Methods, + Dictionary> RootConfigTypes, + MethodSpecifier MethodsToGen, + HashSet PrimitivesForHelperGen, HashSet Namespaces) { - private MethodSpecifier? _methodsToGen; - public MethodSpecifier MethodsToGen - { - get - { - if (!_methodsToGen.HasValue) - { - _methodsToGen = MethodSpecifier.None; - - foreach (KeyValuePair> method in Methods) - { - if (method.Value.Count > 0) - { - MethodSpecifier specifier = method.Key; - - if (specifier is MethodSpecifier.Configure or MethodSpecifier.Get) - { - _methodsToGen |= MethodSpecifier.HasValueOrChildren; - } - else if (specifier is MethodSpecifier.BindCore) - { - _methodsToGen |= MethodSpecifier.HasChildren; - } - - _methodsToGen |= specifier; - } - } - } - - return _methodsToGen.Value; - } - } - } - - [Flags] - internal enum MethodSpecifier - { - None = 0x0, - Bind = 0x1, - Get = 0x2, - Configure = 0x4, - BindCore = 0x8, - HasValueOrChildren = 0x10, - HasChildren = 0x20, } } 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 a2d3a8fdf96474..abd3af76290229 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -1543,11 +1543,13 @@ public void TypeWithPrimitives_Pass() 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); + Assert.Equal(Guid.Parse("e905a75b-d195-494d-8938-e55dcee44574"), obj.Prop24); Uri.TryCreate("https://microsoft.com", UriKind.RelativeOrAbsolute, out Uri? value); Assert.Equal(value, obj.Prop25); +#if BUILDING_SOURCE_GENERATOR_TESTS Assert.Equal(Version.Parse("4.3.2.1"), obj.Prop26); - Assert.Equal(CultureInfo.GetCultureInfoByIetfLanguageTag("yo-NG"), obj.Prop17); +#endif + Assert.Equal(CultureInfo.GetCultureInfo("yo-NG"), obj.Prop17); #if NETCOREAPP data = @"{ @@ -1568,25 +1570,5 @@ public void TypeWithPrimitives_Pass() 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 495b8ecc9ad9d6..8f615985702f29 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 @@ -29,15 +29,8 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration 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); - } + element = ParseInt(stringValue0, () => section.Path); + obj.Add(element); } } } @@ -103,14 +96,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration if (configuration["MyInt"] is string stringValue4) { - 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); - } + obj.MyInt = ParseInt(stringValue4, () => configuration.GetSection("MyInt").Path); } IConfigurationSection section5 = configuration.GetSection("MyList"); @@ -149,5 +135,17 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration } return false; } + + public static int ParseInt(string stringValue, Func getPath) + { + try + { + return int.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(int)}'.", exception); + } + } } } 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 3f90f909cc4288..735f5247fedd7d 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 @@ -50,15 +50,8 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration 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); - } + element = ParseInt(stringValue1, () => section.Path); + obj.Add(element); } } } @@ -98,14 +91,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration if (configuration["MyInt"] is string stringValue4) { - 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); - } + obj.MyInt = ParseInt(stringValue4, () => configuration.GetSection("MyInt").Path); } IConfigurationSection section5 = configuration.GetSection("MyList"); @@ -144,5 +130,17 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration } return false; } + + public static int ParseInt(string stringValue, Func getPath) + { + try + { + return int.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(int)}'.", exception); + } + } } } 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 c60c671f7268a0..bdb48ea091890d 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 @@ -49,15 +49,8 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration 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); - } + element = ParseInt(stringValue1, () => section.Path); + obj.Add(element); } } } @@ -97,14 +90,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration if (configuration["MyInt"] is string stringValue4) { - 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); - } + obj.MyInt = ParseInt(stringValue4, () => configuration.GetSection("MyInt").Path); } IConfigurationSection section5 = configuration.GetSection("MyList"); @@ -143,5 +129,17 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration } return false; } + + public static int ParseInt(string stringValue, Func getPath) + { + try + { + return int.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(int)}'.", exception); + } + } } } 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 d396919f46acf2..152b551d7c96ca 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 @@ -3,27 +3,7 @@ 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'"); - } + 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); } namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration @@ -41,138 +21,485 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration throw new ArgumentNullException(nameof(obj)); } - if (configuration["MyString"] is string stringValue1) + if (configuration["Prop0"] is string stringValue0) + { + obj.Prop0 = ParseBool(stringValue0, () => configuration.GetSection("Prop0").Path); + } + + if (configuration["Prop1"] is string stringValue1) + { + obj.Prop1 = ParseByte(stringValue1, () => configuration.GetSection("Prop1").Path); + } + + if (configuration["Prop2"] is string stringValue2) + { + obj.Prop2 = ParseSbyte(stringValue2, () => configuration.GetSection("Prop2").Path); + } + + if (configuration["Prop3"] is string stringValue3) + { + obj.Prop3 = ParseChar(stringValue3, () => configuration.GetSection("Prop3").Path); + } + + if (configuration["Prop4"] is string stringValue4) + { + obj.Prop4 = ParseDouble(stringValue4, () => configuration.GetSection("Prop4").Path); + } + + if (configuration["Prop5"] is string stringValue5) + { + obj.Prop5 = stringValue5; + } + + if (configuration["Prop6"] is string stringValue6) + { + obj.Prop6 = ParseInt(stringValue6, () => configuration.GetSection("Prop6").Path); + } + + if (configuration["Prop8"] is string stringValue7) + { + obj.Prop8 = ParseShort(stringValue7, () => configuration.GetSection("Prop8").Path); + } + + if (configuration["Prop9"] is string stringValue8) + { + obj.Prop9 = ParseLong(stringValue8, () => configuration.GetSection("Prop9").Path); + } + + if (configuration["Prop10"] is string stringValue9) + { + obj.Prop10 = ParseFloat(stringValue9, () => configuration.GetSection("Prop10").Path); + } + + if (configuration["Prop13"] is string stringValue10) + { + obj.Prop13 = ParseUshort(stringValue10, () => configuration.GetSection("Prop13").Path); + } + + if (configuration["Prop14"] is string stringValue11) + { + obj.Prop14 = ParseUint(stringValue11, () => configuration.GetSection("Prop14").Path); + } + + if (configuration["Prop15"] is string stringValue12) + { + obj.Prop15 = ParseUlong(stringValue12, () => configuration.GetSection("Prop15").Path); + } + + if (configuration["Prop16"] is string stringValue13) + { + obj.Prop16 = stringValue13; + } + + if (configuration["Prop17"] is string stringValue14) + { + obj.Prop17 = ParseCultureInfo(stringValue14, () => configuration.GetSection("Prop17").Path); + } + + if (configuration["Prop19"] is string stringValue15) + { + obj.Prop19 = ParseDateTime(stringValue15, () => configuration.GetSection("Prop19").Path); + } + + if (configuration["Prop20"] is string stringValue16) + { + obj.Prop20 = ParseDateTimeOffset(stringValue16, () => configuration.GetSection("Prop20").Path); + } + + if (configuration["Prop21"] is string stringValue17) + { + obj.Prop21 = ParseDecimal(stringValue17, () => configuration.GetSection("Prop21").Path); + } + + if (configuration["Prop23"] is string stringValue18) + { + obj.Prop23 = ParseTimeSpan(stringValue18, () => configuration.GetSection("Prop23").Path); + } + + if (configuration["Prop24"] is string stringValue19) + { + obj.Prop24 = ParseGuid(stringValue19, () => configuration.GetSection("Prop24").Path); + } + + if (configuration["Prop25"] is string stringValue20) + { + obj.Prop25 = ParseUri(stringValue20, () => configuration.GetSection("Prop25").Path); + } + + if (configuration["Prop26"] is string stringValue21) + { + obj.Prop26 = ParseVersion(stringValue21, () => configuration.GetSection("Prop26").Path); + } + + if (configuration["Prop27"] is string stringValue22) + { + obj.Prop27 = ParseDayOfWeek(stringValue22, () => configuration.GetSection("Prop27").Path); + } + + if (configuration["Prop7"] is string stringValue23) { - obj.MyString = stringValue1; + obj.Prop7 = ParseInt128(stringValue23, () => configuration.GetSection("Prop7").Path); } - if (configuration["MyInt128"] is string stringValue2) + if (configuration["Prop11"] is string stringValue24) { - 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); - } + obj.Prop11 = ParseHalf(stringValue24, () => configuration.GetSection("Prop11").Path); } - if (configuration["MyInt"] is string stringValue3) + if (configuration["Prop12"] is string stringValue25) { - 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); - } + obj.Prop12 = ParseUInt128(stringValue25, () => configuration.GetSection("Prop12").Path); } - if (configuration["MyUInt128"] is string stringValue4) + if (configuration["Prop18"] is string stringValue26) { - 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); - } + obj.Prop18 = ParseDateOnly(stringValue26, () => configuration.GetSection("Prop18").Path); } - if (configuration["MyLong"] is string stringValue5) + if (configuration["Prop22"] is string stringValue27) { - 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); - } + obj.Prop22 = ParseTimeOnly(stringValue27, () => configuration.GetSection("Prop22").Path); } - if (configuration["MyUri"] is string stringValue6) + if (configuration["Prop22"] is string stringValue28) { - Uri.TryCreate(stringValue6, UriKind.RelativeOrAbsolute, out Uri? temp7); - if (temp7 is not null) - { - obj.MyUri = temp7; - } + obj.Prop22 = ParseByteArray(stringValue28, () => configuration.GetSection("Prop22").Path); } - if (configuration["MyCultureInfo"] is string stringValue8) + if (configuration["Prop23"] is string stringValue29) { - 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); - } + obj.Prop23 = ParseInt(stringValue29, () => configuration.GetSection("Prop23").Path); } - if (configuration["MyHalf"] is string stringValue9) + if (configuration["Prop24"] is string stringValue30) { - 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); - } + obj.Prop24 = ParseDateTime(stringValue30, () => configuration.GetSection("Prop24").Path); } + } - if (configuration["MyBool"] is string stringValue10) + + public static bool ParseBool(string stringValue, Func getPath) + { + try { - 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); - } + return bool.Parse(stringValue); } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(bool)}'.", exception); + } + } - if (configuration["MyObject"] is string stringValue11) + public static byte ParseByte(string stringValue, Func getPath) + { + try { - obj.MyObject = stringValue11; + return byte.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(byte)}'.", exception); + } + } - if (configuration["MyByteArray"] is string stringValue12) + public static sbyte ParseSbyte(string stringValue, Func getPath) + { + try { - 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); - } + return sbyte.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(sbyte)}'.", exception); } } - public static bool HasValueOrChildren(IConfiguration configuration) + public static char ParseChar(string stringValue, Func getPath) { - if ((configuration as IConfigurationSection)?.Value is not null) + try + { + return char.Parse(stringValue); + } + catch (Exception exception) { - return true; + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(char)}'.", exception); } - return HasChildren(configuration); } - public static bool HasChildren(IConfiguration configuration) + public static double ParseDouble(string stringValue, Func getPath) { - foreach (IConfigurationSection section in configuration.GetChildren()) + try + { + return double.Parse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(double)}'.", exception); + } + } + + public static int ParseInt(string stringValue, Func getPath) + { + try + { + return int.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(int)}'.", exception); + } + } + + public static short ParseShort(string stringValue, Func getPath) + { + try + { + return short.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(short)}'.", exception); + } + } + + public static long ParseLong(string stringValue, Func getPath) + { + try + { + return long.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(long)}'.", exception); + } + } + + public static float ParseFloat(string stringValue, Func getPath) + { + try + { + return float.Parse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(float)}'.", exception); + } + } + + public static ushort ParseUshort(string stringValue, Func getPath) + { + try + { + return ushort.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ushort)}'.", exception); + } + } + + public static uint ParseUint(string stringValue, Func getPath) + { + try + { + return uint.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(uint)}'.", exception); + } + } + + public static ulong ParseUlong(string stringValue, Func getPath) + { + try + { + return ulong.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(ulong)}'.", exception); + } + } + + public static CultureInfo ParseCultureInfo(string stringValue, Func getPath) + { + try + { + return CultureInfo.GetCultureInfo(stringValue); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(CultureInfo)}'.", exception); + } + } + + public static DateTime ParseDateTime(string stringValue, Func getPath) + { + try + { + return DateTime.Parse(stringValue, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(DateTime)}'.", exception); + } + } + + public static DateTimeOffset ParseDateTimeOffset(string stringValue, Func getPath) + { + try + { + return DateTimeOffset.Parse(stringValue, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(DateTimeOffset)}'.", exception); + } + } + + public static decimal ParseDecimal(string stringValue, Func getPath) + { + try + { + return decimal.Parse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(decimal)}'.", exception); + } + } + + public static TimeSpan ParseTimeSpan(string stringValue, Func getPath) + { + try + { + return TimeSpan.Parse(stringValue, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(TimeSpan)}'.", exception); + } + } + + public static Guid ParseGuid(string stringValue, Func getPath) + { + try + { + return Guid.Parse(stringValue); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(Guid)}'.", exception); + } + } + + public static Uri ParseUri(string stringValue, Func getPath) + { + try + { + return new Uri(stringValue, UriKind.RelativeOrAbsolute); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(Uri)}'.", exception); + } + } + + public static Version ParseVersion(string stringValue, Func getPath) + { + try + { + return Version.Parse(stringValue); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(Version)}'.", exception); + } + } + + public static DayOfWeek ParseDayOfWeek(string stringValue, Func getPath) + { + try + { + return (DayOfWeek)Enum.Parse(typeof(DayOfWeek), stringValue, ignoreCase: true); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(DayOfWeek)}'.", exception); + } + } + + public static Int128 ParseInt128(string stringValue, Func getPath) + { + try + { + return Int128.Parse(stringValue, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(Int128)}'.", exception); + } + } + + public static Half ParseHalf(string stringValue, Func getPath) + { + try + { + return Half.Parse(stringValue, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(Half)}'.", exception); + } + } + + public static UInt128 ParseUInt128(string stringValue, Func getPath) + { + try + { + return UInt128.Parse(stringValue, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(UInt128)}'.", exception); + } + } + + public static DateOnly ParseDateOnly(string stringValue, Func getPath) + { + try + { + return DateOnly.Parse(stringValue, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(DateOnly)}'.", exception); + } + } + + public static TimeOnly ParseTimeOnly(string stringValue, Func getPath) + { + try + { + return TimeOnly.Parse(stringValue, CultureInfo.InvariantCulture); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(TimeOnly)}'.", exception); + } + } + + public static byte[] ParseByteArray(string stringValue, Func getPath) + { + try + { + return Convert.FromBase64String(stringValue); + } + catch (Exception exception) { - return true; + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(byte[])}'.", exception); } - 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 9c8854433d73a3..5120cfa062dc69 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs @@ -119,7 +119,6 @@ public async Task TestBaseline_TestPrimitivesGen() { string testSourceCode = """ using System; - using System.Collections.Generic; using System.Globalization; using Microsoft.Extensions.Configuration; @@ -130,22 +129,43 @@ public static void Main() ConfigurationBuilder configurationBuilder = new(); IConfigurationRoot config = configurationBuilder.Build(); - MyClass options = config.Get(); + MyClass obj = new(); + config.Bind(obj); } 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; } + 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; } + 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; } + public byte[] Prop22 { get; set; } + public int Prop23 { get; set; } + public DateTime Prop24 { get; set; } } } """; @@ -179,11 +199,6 @@ 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); }