From 844c2a9f9d6732c6ec43d28f2fa178c8d6785eff Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Thu, 11 May 2023 14:03:01 -0700 Subject: [PATCH] Support ConfigurationBinder.GetValue in source-gen (#86076) * Support ConfigurationBinder.GetValue in source-gen * Address feedback --- ...igurationBindingSourceGenerator.Emitter.cs | 73 +++++-- ...igurationBindingSourceGenerator.Helpers.cs | 1 - ...figurationBindingSourceGenerator.Parser.cs | 181 ++++++++---------- .../gen/ExceptionMessages.cs | 1 + .../gen/MethodSpecifier.cs | 2 +- .../gen/SourceGenerationSpec.cs | 6 +- .../ConfigurationBinderTests.Collections.cs | 2 + .../tests/Common/ConfigurationBinderTests.cs | 66 +++++-- .../Baselines/TestGetCallGen.generated.txt | 8 +- .../TestGetValueCallGen.generated.txt | 120 ++++++++++++ ...nfingurationBindingSourceGeneratorTests.cs | 37 ++++ 11 files changed, 361 insertions(+), 136 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetValueCallGen.generated.txt 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 7c22fab295c6d..d68c5c8b1fcc7 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs @@ -39,6 +39,7 @@ public static class Identifier public const string binderOptions = nameof(binderOptions); public const string configureActions = nameof(configureActions); public const string configuration = nameof(configuration); + public const string defaultValue = nameof(defaultValue); public const string element = nameof(element); public const string enumValue = nameof(enumValue); public const string exception = nameof(exception); @@ -126,7 +127,7 @@ public Emitter(SourceProductionContext context, SourceGenerationSpec generationS public void Emit() { - if (!_generationSpec.ShouldEmitMethods(MethodSpecifier.GetMethods | MethodSpecifier.BindMethods | MethodSpecifier.Configure)) + if (!_generationSpec.HasRootMethods()) { return; } @@ -160,6 +161,7 @@ public void Emit() _writer.WriteBlockStart($"internal static class {Identifier.Helpers}"); EmitGetCoreMethod(); + EmitGetValueCoreMethod(); EmitBindCoreMethods(); EmitHelperMethods(); @@ -217,7 +219,7 @@ private void EmitGetMethods() { EmitBlankLineIfRequired(); _writer.WriteLine($"public static T? {Identifier.Get}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureActions}) => " + - $"(T?)((T){expressionForGetCore}({Identifier.configuration}, typeof(T), {Identifier.configureActions}) ?? default(T));"); + $"(T?)({expressionForGetCore}({Identifier.configuration}, typeof(T), {Identifier.configureActions}) ?? default(T));"); } if (_generationSpec.ShouldEmitMethods(MethodSpecifier.Get_TypeOf)) @@ -231,38 +233,40 @@ private void EmitGetMethods() { EmitBlankLineIfRequired(); _writer.WriteLine($"public static object? {Identifier.Get}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.Type} {Identifier.type}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureActions}) => " + - $"{expressionForGetCore}({Identifier.configuration}, type, {Identifier.configureActions});"); + $"{expressionForGetCore}({Identifier.configuration}, {Identifier.type}, {Identifier.configureActions});"); } } private void EmitGetValueMethods() { + const string expressionForGetValueCore = $"{FullyQualifiedDisplayName.Helpers}.{Identifier.GetValueCore}"; + if (_generationSpec.ShouldEmitMethods(MethodSpecifier.GetValue_T_key)) { EmitBlankLineIfRequired(); _writer.WriteLine($"public static T? {Identifier.GetValue}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, string {Identifier.key}) => " + - $"throw new {FullyQualifiedDisplayName.NotSupportedException}();"); + $"(T?)({expressionForGetValueCore}({Identifier.configuration}, typeof(T), {Identifier.key}) ?? default(T));"); } if (_generationSpec.ShouldEmitMethods(MethodSpecifier.GetValue_T_key_defaultValue)) { EmitBlankLineIfRequired(); - _writer.WriteLine($"public static T? {Identifier.GetValue}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureActions}) => " + - $"throw new NotSupportedException();"); + _writer.WriteLine($"public static T? {Identifier.GetValue}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, string {Identifier.key}, T {Identifier.defaultValue}) => " + + $"(T?)({expressionForGetValueCore}({Identifier.configuration}, typeof(T), {Identifier.key}) ?? {Identifier.defaultValue});"); } if (_generationSpec.ShouldEmitMethods(MethodSpecifier.GetValue_TypeOf_key)) { EmitBlankLineIfRequired(); - _writer.WriteLine($"public static object? {Identifier.GetValue}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.Type} {Identifier.type}) => " + - $"throw new NotSupportedException();"); + _writer.WriteLine($"public static object? {Identifier.GetValue}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.Type} {Identifier.type}, string {Identifier.key}) => " + + $"{expressionForGetValueCore}({Identifier.configuration}, {Identifier.type}, {Identifier.key});"); } if (_generationSpec.ShouldEmitMethods(MethodSpecifier.GetValue_TypeOf_key_defaultValue)) { EmitBlankLineIfRequired(); - _writer.WriteLine($"public static object? {Identifier.GetValue}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.Type} {Identifier.type}, {FullyQualifiedDisplayName.ActionOfBinderOptions}? {Identifier.configureActions}) =>" + - $"throw new NotSupportedException();"); + _writer.WriteLine($"public static object? {Identifier.GetValue}(this {FullyQualifiedDisplayName.IConfiguration} {Identifier.configuration}, {FullyQualifiedDisplayName.Type} {Identifier.type}, string {Identifier.key}, object? {Identifier.defaultValue}) =>" + + $"{expressionForGetValueCore}({Identifier.configuration}, {Identifier.type}, {Identifier.key}) ?? {Identifier.defaultValue};"); } } @@ -325,7 +329,7 @@ private void EmitGetCoreMethod() return; } - _writer.WriteBlockStart($"public static object {Identifier.GetCore}(this {Identifier.IConfiguration} {Identifier.configuration}, Type {Identifier.type}, Action<{Identifier.BinderOptions}>? {Identifier.configureActions})"); + _writer.WriteBlockStart($"public static object? {Identifier.GetCore}(this {Identifier.IConfiguration} {Identifier.configuration}, Type {Identifier.type}, Action<{Identifier.BinderOptions}>? {Identifier.configureActions})"); EmitCheckForNullArgument_WithBlankLine(Identifier.configuration); @@ -351,8 +355,50 @@ private void EmitGetCoreMethod() _precedingBlockExists = true; } + private void EmitGetValueCoreMethod() + { + if (!_generationSpec.ShouldEmitMethods(MethodSpecifier.GetValueMethods)) + { + return; + } + + EmitBlankLineIfRequired(); + + _writer.WriteBlockStart($"public static object? {Identifier.GetValueCore}(this {Identifier.IConfiguration} {Identifier.configuration}, Type {Identifier.type}, string {Identifier.key})"); + + EmitCheckForNullArgument_WithBlankLine(Identifier.configuration); + + _writer.WriteLine($"{Identifier.IConfigurationSection} {Identifier.section} = {Identifier.configuration}.{Identifier.GetSection}({Identifier.key});"); + _writer.WriteLine($"object? {Identifier.obj};"); + + _writer.WriteBlankLine(); + + foreach (TypeSpec type in _generationSpec.RootConfigTypes[MethodSpecifier.GetValueMethods]) + { + TypeSpec effectiveType = (type as NullableSpec)?.UnderlyingType ?? type; + _writer.WriteBlockStart($"if (type == typeof({type.MinimalDisplayString}))"); + EmitBindLogicFromString( + (ParsableFromStringTypeSpec)effectiveType, + Identifier.obj, + Expression.sectionValue, + Expression.sectionPath, + writeOnSuccess: () => _writer.WriteLine($"return {Identifier.obj};")); + _writer.WriteBlockEnd(); + _writer.WriteBlankLine(); + } + + _writer.WriteLine("return null;"); + _writer.WriteBlockEnd(); + _precedingBlockExists = true; + } + private void EmitBindCoreMethods() { + if (!_generationSpec.ShouldEmitMethods(MethodSpecifier.BindCore)) + { + return; + } + foreach (TypeSpec type in _generationSpec.RootConfigTypes[MethodSpecifier.BindCore]) { if (type.SpecKind is TypeSpecKind.ParsableFromString) @@ -1047,7 +1093,7 @@ private void EmitCastToIConfigurationSection() private void EmitIConfigurationHasValueOrChildrenCheck(bool voidReturn) { - string returnPostfix = voidReturn ? "" : " default!"; + string returnPostfix = voidReturn ? string.Empty : " null"; _writer.WriteBlock($$""" if (!{{GetHelperMethodDisplayString(Identifier.HasValueOrChildren)}}({{Identifier.configuration}})) @@ -1078,6 +1124,9 @@ private void EmitBlankLineIfRequired() private void Emit_NotSupportedException_TypeNotDetectedAsInput() => _writer.WriteLine(@$"throw new global::System.NotSupportedException($""{string.Format(ExceptionMessages.TypeNotDetectedAsInput, "{type}")}"");"); + private void Emit_NotSupportedExceptionTypeNotSupportedAsInput() => + _writer.WriteLine(@$"throw new global::System.NotSupportedException($""{string.Format(ExceptionMessages.TypeNotSupportedAsInput, "{type}")}"");"); + private void EmitCheckForNullArgument_WithBlankLine_IfRequired(bool isValueType) { if (!isValueType) 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 aab2c91f79041..b8d5dec48c7ad 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs @@ -2,7 +2,6 @@ // 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 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 c72b1876a26e4..45d71cd60d982 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs @@ -236,7 +236,7 @@ private void ProcessGetValueCall(BinderInvocationOperation binderOperation) int paramLength = @params.Length; MethodSpecifier binderMethod = MethodSpecifier.None; - INamedTypeSymbol? namedType; + ITypeSymbol? type; if (targetMethod.IsGenericMethod) { @@ -245,15 +245,15 @@ private void ProcessGetValueCall(BinderInvocationOperation binderOperation) return; } - namedType = targetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None) as INamedTypeSymbol; + type = targetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None); if (paramLength is 2) { binderMethod = MethodSpecifier.GetValue_T_key; } - else if (paramLength is 3 && Helpers.TypesAreEqual(@params[2].Type, namedType)) + else if (paramLength is 3 && Helpers.TypesAreEqual(@params[2].Type, type)) { - binderMethod = MethodSpecifier.Get_T_BinderOptions; + binderMethod = MethodSpecifier.GetValue_T_key_defaultValue; } } else if (paramLength > 4) @@ -268,27 +268,31 @@ private void ProcessGetValueCall(BinderInvocationOperation binderOperation) } ITypeOfOperation? typeOfOperation = operation.Arguments[1].ChildOperations.FirstOrDefault() as ITypeOfOperation; - namedType = typeOfOperation?.TypeOperand as INamedTypeSymbol; + type = typeOfOperation?.TypeOperand; if (paramLength is 3) { - binderMethod = MethodSpecifier.Get_TypeOf; + binderMethod = MethodSpecifier.GetValue_TypeOf_key; } - else if (paramLength is 4 && Helpers.TypesAreEqual(@params[3].Type, namedType)) + else if (paramLength is 4 && @params[3].Type.SpecialType is SpecialType.System_Object) { - binderMethod = MethodSpecifier.Get_TypeOf_BinderOptions; + binderMethod = MethodSpecifier.GetValue_TypeOf_key_defaultValue; } } if (binderMethod is MethodSpecifier.None || - namedType is null || - namedType.SpecialType == SpecialType.System_Object || - namedType.SpecialType == SpecialType.System_Void) + type is null || + type.SpecialType == SpecialType.System_Object || + type.SpecialType == SpecialType.System_Void) { return; } - AddRootConfigType(MethodSpecifier.GetValueMethods, binderMethod, namedType, binderOperation.Location); + ITypeSymbol effectiveType = IsNullable(type, out ITypeSymbol? underlyingType) ? underlyingType : type; + if (IsParsableFromString(effectiveType, out _)) + { + AddRootConfigType(MethodSpecifier.GetValueMethods, binderMethod, type, binderOperation.Location); + } } private void ProcessConfigureCall(BinderInvocationOperation binderOperation) @@ -316,12 +320,12 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) private TypeSpec? AddRootConfigType(MethodSpecifier methodGroup, MethodSpecifier method, ITypeSymbol type, Location? location) { - if (type is not INamedTypeSymbol namedType || ContainsGenericParameters(namedType)) + if (type is INamedTypeSymbol namedType && ContainsGenericParameters(namedType)) { return null; } - TypeSpec? spec = GetOrCreateTypeSpec(namedType, location); + TypeSpec? spec = GetOrCreateTypeSpec(type, location); if (spec != null) { GetRootConfigTypeCache(method).Add(spec); @@ -340,110 +344,67 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) return spec; } - if (type is INamedTypeSymbol { IsGenericType: true } genericType && - genericType.ConstructUnboundGenericType() is INamedTypeSymbol { } unboundGeneric && - unboundGeneric.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + if (IsNullable(type, out ITypeSymbol? underlyingType)) { - return TryGetTypeSpec(genericType.TypeArguments[0], Helpers.NullableUnderlyingTypeNotSupported, out TypeSpec? underlyingType) - ? CacheSpec(new NullableSpec(type) { Location = location, UnderlyingType = underlyingType }) + spec = TryGetTypeSpec(underlyingType, Helpers.NullableUnderlyingTypeNotSupported, out TypeSpec? underlyingTypeSpec) + ? new NullableSpec(type) { Location = location, UnderlyingType = underlyingTypeSpec } : null; } - else if (IsSupportedArrayType(type, location, out ITypeSymbol? elementType)) + else if (IsParsableFromString(type, out StringParsableTypeKind specialTypeKind)) { - if (elementType.SpecialType is SpecialType.System_Byte) + ParsableFromStringTypeSpec stringParsableSpec = new(type) { - return CacheSpec(new ParsableFromStringTypeSpec(type) { Location = location, StringParsableTypeKind = StringParsableTypeKind.ByteArray }); - } + Location = location, + StringParsableTypeKind = specialTypeKind + }; - spec = CreateArraySpec((type as IArrayTypeSymbol)!, location); - if (spec is null) + if (stringParsableSpec.StringParsableTypeKind is not StringParsableTypeKind.ConfigValue) { - return null; + _primitivesForHelperGen.Add(stringParsableSpec); } - return CacheSpec(spec); + spec = stringParsableSpec; } - else if (IsParsableFromString(type, out StringParsableTypeKind specialTypeKind)) + else if (IsSupportedArrayType(type, location)) { - return CacheSpec( - new ParsableFromStringTypeSpec(type) - { - Location = location, - StringParsableTypeKind = specialTypeKind - }); + spec = CreateArraySpec((type as IArrayTypeSymbol)!, location); + RegisterBindCoreGenType(spec); } else if (IsCollection(type)) { spec = CreateCollectionSpec((INamedTypeSymbol)type, location); - if (spec is null) - { - return null; - } - - return CacheSpec(spec); + RegisterBindCoreGenType(spec); } else if (Helpers.TypesAreEqual(type, _typeSymbols.IConfigurationSection)) { - return CacheSpec(new ConfigurationSectionTypeSpec(type) { Location = location }); + spec = new ConfigurationSectionTypeSpec(type) { Location = location }; } else if (type is INamedTypeSymbol namedType) { spec = CreateObjectSpec(namedType, location); - if (spec is null) - { - return null; - } - - return CacheSpec(spec); + RegisterBindCoreGenType(spec); } - ReportUnsupportedType(type, Helpers.TypeNotSupported, location); - return null; - - T CacheSpec(T? spec) where T : TypeSpec + if (spec is null) { - TypeSpecKind typeKind = spec.SpecKind; - Debug.Assert(typeKind is not TypeSpecKind.Unknown); - - string @namespace = spec.Namespace; - if (@namespace != null && @namespace != "") - { - _namespaces.Add(@namespace); - } + ReportUnsupportedType(type, Helpers.TypeNotSupported, location); + return null; + } - HashSet bindCoreTypeCache = GetRootConfigTypeCache(MethodSpecifier.BindCore); - switch (spec) - { - case ParsableFromStringTypeSpec stringParsableSpec: - { - if (stringParsableSpec.StringParsableTypeKind is not StringParsableTypeKind.ConfigValue) - { - _primitivesForHelperGen.Add(stringParsableSpec); - } - } - break; - case ObjectSpec: - case DictionarySpec: - case CollectionSpec: - { - RegisterBindCoreGenType(spec); - } - break; - case NullableSpec nullableSpec: - { - RegisterBindCoreGenType(nullableSpec.UnderlyingType); - } - break; - default: - break; - } + string @namespace = spec.Namespace; + if (@namespace is not null and not "") + { + _namespaces.Add(@namespace); + } - _createdSpecs[type] = spec; - return spec; + _createdSpecs[type] = spec; + return spec; - void RegisterBindCoreGenType(TypeSpec spec) + void RegisterBindCoreGenType(TypeSpec? spec) + { + if (spec is not null) { - bindCoreTypeCache.Add(spec); + GetRootConfigTypeCache(MethodSpecifier.BindCore).Add(spec); _methodsToGen |= MethodSpecifier.BindCore; } } @@ -459,8 +420,28 @@ private HashSet GetRootConfigTypeCache(MethodSpecifier method) return types; } + private static bool IsNullable(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? underlyingType) + { + if (type is INamedTypeSymbol { IsGenericType: true } genericType && + genericType.ConstructUnboundGenericType() is INamedTypeSymbol { } unboundGeneric && + unboundGeneric.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + underlyingType = genericType.TypeArguments[0]; + return true; + } + + underlyingType = null; + return false; + } + private bool IsParsableFromString(ITypeSymbol type, out StringParsableTypeKind typeKind) { + if (type is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }) + { + typeKind = StringParsableTypeKind.ByteArray; + return true; + } + if (type is not INamedTypeSymbol namedType) { typeKind = StringParsableTypeKind.None; @@ -577,8 +558,8 @@ private bool TryGetTypeSpec(ITypeSymbol type, DiagnosticDescriptor descriptor, o return null; } - // 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 want a BindCore method for List as a temp holder for the array values. + EnumerableSpec? listSpec = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.List, arrayType.ElementType) as EnumerableSpec; // We know the element type is supported. Debug.Assert(listSpec != null); @@ -590,22 +571,19 @@ private bool TryGetTypeSpec(ITypeSymbol type, DiagnosticDescriptor descriptor, o }; } - private bool IsSupportedArrayType(ITypeSymbol type, Location? location, [NotNullWhen(true)] out ITypeSymbol? elementType) + private bool IsSupportedArrayType(ITypeSymbol type, Location? location) { if (type is not IArrayTypeSymbol arrayType) { - elementType = null; return false; } if (arrayType.Rank > 1) { ReportUnsupportedType(arrayType, Helpers.MultiDimArraysNotSupported, location); - elementType = null; return false; } - elementType = arrayType.ElementType; return true; } @@ -641,7 +619,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc if (IsInterfaceMatch(type, _typeSymbols.GenericIDictionary) || IsInterfaceMatch(type, _typeSymbols.IDictionary)) { // We know the key and element types are supported. - concreteType = ConstructAndCacheGenericTypeForBind(_typeSymbols.Dictionary, keyType, elementType) as DictionarySpec; + concreteType = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.Dictionary, keyType, elementType) as DictionarySpec; Debug.Assert(concreteType != null); } else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType, keyType)) @@ -660,10 +638,12 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc }; } - private TypeSpec? ConstructAndCacheGenericTypeForBind(INamedTypeSymbol type, params ITypeSymbol[] parameters) + private TypeSpec? ConstructAndCacheGenericTypeForBindCore(INamedTypeSymbol type, params ITypeSymbol[] parameters) { Debug.Assert(type.IsGenericType); - return AddRootConfigType(MethodSpecifier.BindMethods, MethodSpecifier.Bind_instance, type.Construct(parameters), location: null); + TypeSpec spec = GetOrCreateTypeSpec(type.Construct(parameters)); + GetRootConfigTypeCache(MethodSpecifier.BindCore).Add(spec); + return spec; } private EnumerableSpec? CreateEnumerableSpec(INamedTypeSymbol type, Location? location, ITypeSymbol elementType) @@ -672,16 +652,15 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc { return null; } - EnumerableSpec? concreteType = null; if (IsInterfaceMatch(type, _typeSymbols.ISet)) { - concreteType = ConstructAndCacheGenericTypeForBind(_typeSymbols.HashSet, elementType) as EnumerableSpec; + concreteType = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.HashSet, elementType) as EnumerableSpec; } else if (IsInterfaceMatch(type, _typeSymbols.ICollection) || IsInterfaceMatch(type, _typeSymbols.GenericIList)) { - concreteType = ConstructAndCacheGenericTypeForBind(_typeSymbols.List, elementType) as EnumerableSpec; + concreteType = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.List, elementType) as EnumerableSpec; } else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType)) { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ExceptionMessages.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ExceptionMessages.cs index 511afdf8e84ec..a90f015ec8ea2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ExceptionMessages.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ExceptionMessages.cs @@ -10,5 +10,6 @@ internal static class ExceptionMessages public const string FailedBinding = "Failed to convert configuration value at '{0}' to type '{1}'."; public const string MissingConfig = "'{0}' was set on the provided {1}, but the following properties were not found on the instance of {2}: {3}"; public const string TypeNotDetectedAsInput = "Unable to bind to type '{0}': generator did not detect the type as input."; + public const string TypeNotSupportedAsInput = "Unable to bind to type '{0}': generator does not support this type as input to this method."; } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/MethodSpecifier.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/MethodSpecifier.cs index ce60e0703972f..2237a70f73e01 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/MethodSpecifier.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/MethodSpecifier.cs @@ -78,7 +78,7 @@ internal enum MethodSpecifier // Method groups BindMethods = Bind_instance | Bind_instance_BinderOptions | Bind_key_instance, GetMethods = Get_T | Get_T_BinderOptions | Get_TypeOf | Get_TypeOf_BinderOptions, - GetValueMethods = Get_T | Get_T_BinderOptions | Get_TypeOf | Get_TypeOf_BinderOptions, + GetValueMethods = GetValue_T_key | GetValue_T_key_defaultValue | GetValue_TypeOf_key | GetValue_TypeOf_key_defaultValue, RootMethodsWithConfigOptions = Bind_instance_BinderOptions | Get_T_BinderOptions | Get_TypeOf_BinderOptions, } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs index 3babbc87ce67f..d6d8afd37bf27 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs @@ -11,7 +11,9 @@ internal sealed record SourceGenerationSpec( HashSet PrimitivesForHelperGen, HashSet Namespaces) { - public bool ShouldEmitMethods(MethodSpecifier methods) - => (MethodsToGen & methods) != 0; + public bool HasRootMethods() => + ShouldEmitMethods(MethodSpecifier.GetMethods | MethodSpecifier.BindMethods | MethodSpecifier.Configure | MethodSpecifier.GetValueMethods); + + public bool ShouldEmitMethods(MethodSpecifier methods) => (MethodsToGen & methods) != 0; } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs index 3b578aa8ce88e..619a11627683d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; +#if BUILDING_SOURCE_GENERATOR_TESTS using Microsoft.Extensions.Configuration; +#endif using Xunit; namespace Microsoft.Extensions 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 ab49c11fab3e8..c025e194c38cd 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -193,13 +193,37 @@ public void EmptyStringIsNullable() var config = configurationBuilder.Build(); #if BUILDING_SOURCE_GENERATOR_TESTS - Assert.Throws(() => config.GetValue("empty")); + // Ensure exception messages are in sync + Assert.Throws(() => config.GetValue("empty")); + Assert.Throws(() => config.GetValue("empty")); #else Assert.Null(config.GetValue("empty")); Assert.Null(config.GetValue("empty")); #endif } + [Fact] + public void GetScalar() + { + var dic = new Dictionary + { + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + Assert.True(config.GetValue("Boolean")); + Assert.Equal(-2, config.GetValue("Integer")); + Assert.Equal(11, config.GetValue("Nested:Integer")); + + Assert.True((bool)config.GetValue(typeof(bool), "Boolean")); + Assert.Equal(-2, (int)config.GetValue(typeof(int), "Integer")); + Assert.Equal(11, (int)config.GetValue(typeof(int), "Nested:Integer")); + } + [Fact] public void GetScalarNullable() { @@ -213,13 +237,13 @@ public void GetScalarNullable() configurationBuilder.AddInMemoryCollection(dic); var config = configurationBuilder.Build(); -#if BUILDING_SOURCE_GENERATOR_TESTS - Assert.Throws(() => config.GetValue("Boolean")); -#else Assert.True(config.GetValue("Boolean")); Assert.Equal(-2, config.GetValue("Integer")); Assert.Equal(11, config.GetValue("Nested:Integer")); -#endif + + Assert.True((bool)config.GetValue(typeof(bool?), "Boolean")); + Assert.Equal(-2, (int)config.GetValue(typeof(int?), "Integer")); + Assert.Equal(11, (int)config.GetValue(typeof(int?), "Nested:Integer")); } [Fact] @@ -253,17 +277,31 @@ public void GetNullValue() configurationBuilder.AddInMemoryCollection(dic); var config = configurationBuilder.Build(); -#if BUILDING_SOURCE_GENERATOR_TESTS - Assert.Throws(() => config.GetValue("Boolean")); - Assert.Throws(() => config.GetValue("Integer")); - Assert.Throws(() => config.GetValue("Nested:Integer")); - Assert.Throws(() => config.GetValue("Object")); -#else + // Generic overloads. Assert.False(config.GetValue("Boolean")); Assert.Equal(0, config.GetValue("Integer")); Assert.Equal(0, config.GetValue("Nested:Integer")); Assert.Null(config.GetValue("Object")); -#endif + + // Generic overloads with default value. + Assert.True(config.GetValue("Boolean", true)); + Assert.Equal(1, config.GetValue("Integer", 1)); + Assert.Equal(1, config.GetValue("Nested:Integer", 1)); + Assert.Equal(new NestedConfig(""), config.GetValue("Object", new NestedConfig(""))); + + // Type overloads. + Assert.Null(config.GetValue(typeof(bool), "Boolean")); + Assert.Null(config.GetValue(typeof(int), "Integer")); + Assert.Null(config.GetValue(typeof(int), "Nested:Integer")); + Assert.Null(config.GetValue(typeof(ComplexOptions), "Object")); + + // Type overloads with default value. + Assert.True((bool)config.GetValue(typeof(bool), "Boolean", true)); + Assert.Equal(1, (int)config.GetValue(typeof(int), "Integer", 1)); + Assert.Equal(1, (int)config.GetValue(typeof(int), "Nested:Integer", 1)); + Assert.Equal(new NestedConfig(""), config.GetValue("Object", new NestedConfig(""))); + + // GetSection tests. Assert.False(config.GetSection("Boolean").Get()); Assert.Equal(0, config.GetSection("Integer").Get()); Assert.Equal(0, config.GetSection("Nested:Integer").Get()); @@ -370,7 +408,7 @@ public void ThrowsIfPropertyInConfigMissingInNestedModel_Get() Assert.Equal(expectedMessage, ex.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for GetValue + [Fact] public void GetDefaultsWhenDataDoesNotExist() { var dic = new Dictionary @@ -391,7 +429,7 @@ public void GetDefaultsWhenDataDoesNotExist() Assert.Same(config.GetValue("Object", foo), foo); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for GetValue. + [Fact] public void GetUri() { var dic = new Dictionary 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 57e000db4dd86..882ff2d8a8f41 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 @@ -5,13 +5,11 @@ internal static class GeneratedConfigurationBinder { public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureActions: null) ?? default(T)); - public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Action? configureActions) => (T?)((T)global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureActions) ?? default(T)); + public static T? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Action? configureActions) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureActions) ?? default(T)); public static object? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Type type) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, type, configureActions: null); public static object? Get(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Type type, global::System.Action? configureActions) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, type, configureActions); - - public static void Bind(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Collections.Generic.List obj) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.BindCore(configuration, ref obj, binderOptions: null); } namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration @@ -23,7 +21,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration internal static class Helpers { - public static object GetCore(this IConfiguration configuration, Type type, Action? configureActions) + public static object? GetCore(this IConfiguration configuration, Type type, Action? configureActions) { if (configuration is null) { @@ -34,7 +32,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration if (!HasValueOrChildren(configuration)) { - return default!; + return null; } if (type == typeof(Program.MyClass)) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetValueCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetValueCallGen.generated.txt new file mode 100644 index 0000000000000..cbf8e6f3ef1b5 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetValueCallGen.generated.txt @@ -0,0 +1,120 @@ +// +#nullable enable + +internal static class GeneratedConfigurationBinder +{ + public static T? GetValue(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, string key) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetValueCore(configuration, typeof(T), key) ?? default(T)); + + public static T? GetValue(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, string key, T defaultValue) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetValueCore(configuration, typeof(T), key) ?? defaultValue); + + public static object? GetValue(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Type type, string key) => global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetValueCore(configuration, type, key); + + public static object? GetValue(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, global::System.Type type, string key, object? defaultValue) =>global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetValueCore(configuration, type, key) ?? defaultValue; +} + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + using System; + using System.Globalization; + using Microsoft.Extensions.Configuration; + + internal static class Helpers + { + public static object? GetValueCore(this IConfiguration configuration, Type type, string key) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + IConfigurationSection section = configuration.GetSection(key); + object? obj; + + if (type == typeof(int)) + { + if (section.Value is string stringValue0) + { + obj = ParseInt(stringValue0, () => section.Path); + return obj; + } + } + + if (type == typeof(bool?)) + { + if (section.Value is string stringValue1) + { + obj = ParseBool(stringValue1, () => section.Path); + return obj; + } + } + + if (type == typeof(byte[])) + { + if (section.Value is string stringValue2) + { + obj = ParseByteArray(stringValue2, () => section.Path); + return obj; + } + } + + if (type == typeof(CultureInfo)) + { + if (section.Value is string stringValue3) + { + obj = ParseCultureInfo(stringValue3, () => section.Path); + return obj; + } + } + + return null; + } + + 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 bool ParseBool(string stringValue, Func getPath) + { + try + { + return bool.Parse(stringValue); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(bool)}'.", exception); + } + } + + public static byte[] ParseByteArray(string stringValue, Func getPath) + { + try + { + return Convert.FromBase64String(stringValue); + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(byte[])}'.", 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); + } + } + } +} 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 a5a1f9f3177bf..6359acb9bc44a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs @@ -9,7 +9,9 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +#if NETCOREAPP using Microsoft.Extensions.DependencyInjection; +#endif using SourceGenerators.Tests; using Xunit; @@ -102,6 +104,41 @@ public class MyClass4 await VerifyAgainstBaselineUsingFile("TestGetCallGen.generated.txt", testSourceCode); } + [Fact] + public async Task TestBaseline_TestGetValueCallGen() + { + string testSourceCode = @" +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(); + + config.GetValue(""key""); + config.GetValue(typeof(bool?), ""key""); + config.GetValue(""key"", new MyClass()); + config.GetValue(""key"", new byte[] { }); + config.GetValue(typeof(CultureInfo), ""key"", CultureInfo.InvariantCulture); + } + + public class MyClass + { + public string MyString { get; set; } + public int MyInt { get; set; } + public List MyList { get; set; } + public int[] MyArray { get; set; } + public Dictionary MyDictionary { get; set; } + } +}"; + + await VerifyAgainstBaselineUsingFile("TestGetValueCallGen.generated.txt", testSourceCode); + } + [Fact] public async Task TestBaseline_TestConfigureCallGen() {