From cadfb5d26f6d0a08d470e85a28b1f68471c08d6e Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Sat, 10 Dec 2022 09:30:35 -0800 Subject: [PATCH 1/7] Create source generator for configuration binding --- docs/project/list-of-diagnostics.md | 14 + ...rosoft.Extensions.Configuration.Binder.sln | 44 +- .../gen/CollectionSpec.cs | 40 + ...igurationBindingSourceGenerator.Emitter.cs | 674 ++++ ...igurationBindingSourceGenerator.Helpers.cs | 149 + ...figurationBindingSourceGenerator.Parser.cs | 655 ++++ .../ConfigurationBindingSourceGenerator.cs | 66 + .../gen/ConstructionStrategy.cs | 11 + ...nfiguration.Binder.SourceGeneration.csproj | 50 + .../gen/NullableSpec.cs | 15 + .../gen/ObjectSpec.cs | 15 + .../gen/PopulationStrategy.cs | 14 + .../gen/PropertySpec.cs | 26 + .../gen/Resources/Strings.resx | 132 + .../gen/Resources/xlf/Strings.cs.xlf | 27 + .../gen/Resources/xlf/Strings.de.xlf | 27 + .../gen/Resources/xlf/Strings.es.xlf | 27 + .../gen/Resources/xlf/Strings.fr.xlf | 27 + .../gen/Resources/xlf/Strings.it.xlf | 27 + .../gen/Resources/xlf/Strings.ja.xlf | 27 + .../gen/Resources/xlf/Strings.ko.xlf | 27 + .../gen/Resources/xlf/Strings.pl.xlf | 27 + .../gen/Resources/xlf/Strings.pt-BR.xlf | 27 + .../gen/Resources/xlf/Strings.ru.xlf | 27 + .../gen/Resources/xlf/Strings.tr.xlf | 27 + .../gen/Resources/xlf/Strings.zh-Hans.xlf | 27 + .../gen/Resources/xlf/Strings.zh-Hant.xlf | 27 + .../gen/SourceGenerationSpec.cs | 12 + .../gen/TypeSpec.cs | 34 + .../gen/TypeSpecKind.cs | 19 + .../src/Properties/InternalsVisibleTo.cs | 1 + .../ConfigurationBinderTests.Collections.cs} | 839 +++-- .../ConfigurationBinderTests.Helpers.cs | 154 + ...tionBinderTests.TestClasses.Collections.cs | 335 ++ .../ConfigurationBinderTests.TestClasses.cs | 570 ++++ .../tests/Common/ConfigurationBinderTests.cs | 1438 ++++++++ .../tests/ConfigurationBinderTests.cs | 2911 ----------------- ...ation.Binder.SourceGeneration.Tests.csproj | 44 + .../tests/{ => Tests}/ILLink.Descriptors.xml | 0 ...tensions.Configuration.Binder.Tests.csproj | 9 +- 40 files changed, 5519 insertions(+), 3103 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConstructionStrategy.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/NullableSpec.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ObjectSpec.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/PopulationStrategy.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/PropertySpec.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/Strings.resx create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.cs.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.de.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.es.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.fr.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.it.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ja.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ko.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pl.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pt-BR.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ru.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.tr.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hans.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hant.xlf create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpec.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpecKind.cs rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{ConfigurationCollectionBindingTests.cs => Common/ConfigurationBinderTests.Collections.cs} (64%) create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs delete mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{ => Tests}/ILLink.Descriptors.xml (100%) rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{ => Tests}/Microsoft.Extensions.Configuration.Binder.Tests.csproj (63%) diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md index 6620ae24a68ad..f0c0b533c0a1c 100644 --- a/docs/project/list-of-diagnostics.md +++ b/docs/project/list-of-diagnostics.md @@ -211,6 +211,20 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL | __`SYSLIB1097`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ | | __`SYSLIB1098`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ | | __`SYSLIB1099`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ | +| __`SYSLIB1100`__ | Configuration binding generator: type is not supported. | +| __`SYSLIB1101`__ | Configuration binding generator: property on type is not supported. | +| __`SYSLIB1102`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1103`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1104`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1105`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1106`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1107`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1108`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1109`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1110`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1111`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1112`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | +| __`SYSLIB1113`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* | ### Diagnostic Suppressions (`SYSLIBSUPPRESS****`) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/Microsoft.Extensions.Configuration.Binder.sln b/src/libraries/Microsoft.Extensions.Configuration.Binder/Microsoft.Extensions.Configuration.Binder.sln index 3b3b52939f277..4961547ca6b05 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/Microsoft.Extensions.Configuration.Binder.sln +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/Microsoft.Extensions.Configuration.Binder.sln @@ -1,4 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 +# Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{5FB89358-3575-45AA-ACFE-EF3598B9AB7E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Abstractions", "..\Microsoft.Extensions.Configuration.Abstractions\ref\Microsoft.Extensions.Configuration.Abstractions.csproj", "{CB09105A-F475-4A91-8836-434FA175F4F9}" @@ -9,8 +10,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Config EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Binder", "src\Microsoft.Extensions.Configuration.Binder.csproj", "{8C7443D8-864A-4DAE-8835-108580C289F5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Binder.Tests", "tests\Microsoft.Extensions.Configuration.Binder.Tests.csproj", "{FD4C7C59-55A7-42C8-9B75-43728FAFDD84}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration", "..\Microsoft.Extensions.Configuration\ref\Microsoft.Extensions.Configuration.csproj", "{39D99379-E744-4295-9CA8-B5C6DE286EA0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration", "..\Microsoft.Extensions.Configuration\src\Microsoft.Extensions.Configuration.csproj", "{5177A566-05AF-4DF0-93CC-D2876F7E6EBB}" @@ -35,6 +34,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{94EEF122-C30 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{CC3961B0-C62D-44B9-91DB-11D94A3F91A5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.SourceGeneration", "gen\Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj", "{D4B3EEA1-7394-49EA-A088-897C0CD26D11}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests", "tests\SourceGeneration.Tests\Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj", "{56F4D38E-41A0-45E2-9F04-9E670D002E71}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.Tests", "tests\Tests\Microsoft.Extensions.Configuration.Binder.Tests.csproj", "{75BA0154-A24D-421E-9046-C7949DF12A55}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,10 +66,6 @@ Global {8C7443D8-864A-4DAE-8835-108580C289F5}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C7443D8-864A-4DAE-8835-108580C289F5}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C7443D8-864A-4DAE-8835-108580C289F5}.Release|Any CPU.Build.0 = Release|Any CPU - {FD4C7C59-55A7-42C8-9B75-43728FAFDD84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD4C7C59-55A7-42C8-9B75-43728FAFDD84}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD4C7C59-55A7-42C8-9B75-43728FAFDD84}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD4C7C59-55A7-42C8-9B75-43728FAFDD84}.Release|Any CPU.Build.0 = Release|Any CPU {39D99379-E744-4295-9CA8-B5C6DE286EA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {39D99379-E744-4295-9CA8-B5C6DE286EA0}.Debug|Any CPU.Build.0 = Debug|Any CPU {39D99379-E744-4295-9CA8-B5C6DE286EA0}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -97,13 +98,36 @@ Global {6E58AF2F-AB35-4279-9135-67E97BCE1432}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E58AF2F-AB35-4279-9135-67E97BCE1432}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E58AF2F-AB35-4279-9135-67E97BCE1432}.Release|Any CPU.Build.0 = Release|Any CPU + {06BAC1EF-96B3-4F9A-A962-D10204EAF6EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06BAC1EF-96B3-4F9A-A962-D10204EAF6EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06BAC1EF-96B3-4F9A-A962-D10204EAF6EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06BAC1EF-96B3-4F9A-A962-D10204EAF6EF}.Release|Any CPU.Build.0 = Release|Any CPU + {C459288D-8A57-456E-B5B3-861C8043EEE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C459288D-8A57-456E-B5B3-861C8043EEE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C459288D-8A57-456E-B5B3-861C8043EEE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C459288D-8A57-456E-B5B3-861C8043EEE0}.Release|Any CPU.Build.0 = Release|Any CPU + {D4B3EEA1-7394-49EA-A088-897C0CD26D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4B3EEA1-7394-49EA-A088-897C0CD26D11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4B3EEA1-7394-49EA-A088-897C0CD26D11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4B3EEA1-7394-49EA-A088-897C0CD26D11}.Release|Any CPU.Build.0 = Release|Any CPU + {EA44B6C3-BC98-4253-8BDE-5848942967A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA44B6C3-BC98-4253-8BDE-5848942967A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA44B6C3-BC98-4253-8BDE-5848942967A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA44B6C3-BC98-4253-8BDE-5848942967A6}.Release|Any CPU.Build.0 = Release|Any CPU + {56F4D38E-41A0-45E2-9F04-9E670D002E71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56F4D38E-41A0-45E2-9F04-9E670D002E71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56F4D38E-41A0-45E2-9F04-9E670D002E71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56F4D38E-41A0-45E2-9F04-9E670D002E71}.Release|Any CPU.Build.0 = Release|Any CPU + {75BA0154-A24D-421E-9046-C7949DF12A55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75BA0154-A24D-421E-9046-C7949DF12A55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75BA0154-A24D-421E-9046-C7949DF12A55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75BA0154-A24D-421E-9046-C7949DF12A55}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {5FB89358-3575-45AA-ACFE-EF3598B9AB7E} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0} - {FD4C7C59-55A7-42C8-9B75-43728FAFDD84} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0} {CB09105A-F475-4A91-8836-434FA175F4F9} = {F43534CA-C419-405E-B239-CDE2BDC703BE} {F05212B2-FD66-4E8E-AEA0-FAC06A0D6808} = {F43534CA-C419-405E-B239-CDE2BDC703BE} {39D99379-E744-4295-9CA8-B5C6DE286EA0} = {F43534CA-C419-405E-B239-CDE2BDC703BE} @@ -116,6 +140,12 @@ Global {9B21B87F-084B-411B-A513-C22B5B961BF3} = {94EEF122-C307-4BF0-88FE-263B89B59F9F} {05B7F752-4991-4DC8-9B06-8269211E7817} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5} {6E58AF2F-AB35-4279-9135-67E97BCE1432} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5} + {06BAC1EF-96B3-4F9A-A962-D10204EAF6EF} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5} + {C459288D-8A57-456E-B5B3-861C8043EEE0} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5} + {D4B3EEA1-7394-49EA-A088-897C0CD26D11} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5} + {EA44B6C3-BC98-4253-8BDE-5848942967A6} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0} + {56F4D38E-41A0-45E2-9F04-9E670D002E71} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0} + {75BA0154-A24D-421E-9046-C7949DF12A55} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A97DC4BF-32F0-46E8-B91C-84D1E7F2A27E} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs new file mode 100644 index 0000000000000..31058c49a009f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/CollectionSpec.cs @@ -0,0 +1,40 @@ +// 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 abstract record CollectionSpec : TypeSpec + { + public CollectionSpec(ITypeSymbol type) : base(type) + { + IsReadOnly = type.IsReadOnly; + IsInterface = type is INamedTypeSymbol { TypeKind: TypeKind.Interface }; + } + + public required TypeSpec ElementType { get; init; } + + public bool IsReadOnly { get; } + + public bool IsInterface { get; } + + public CollectionSpec? ConcreteType { get; init; } + } + + internal sealed record EnumerableSpec : CollectionSpec + { + public EnumerableSpec(ITypeSymbol type) : base(type) { } + + public override TypeSpecKind SpecKind { get; init; } = TypeSpecKind.Enumerable; + } + + internal sealed record DictionarySpec : CollectionSpec + { + public DictionarySpec(INamedTypeSymbol type) : base(type) { } + + public override TypeSpecKind SpecKind => TypeSpecKind.Dictionary; + + public required TypeSpec 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 new file mode 100644 index 0000000000000..ea1e3c646586b --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs @@ -0,0 +1,674 @@ +// 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.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Configuration.Binder.SourceGeneration; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + public sealed partial class ConfigurationBindingSourceGenerator + { + private sealed partial class Emitter + { + private static class Expression + { + public const string sectionKey = "section?.Key"; + public const string sectionValue = "section?.Value"; + } + + private static class GlobalName + { + public const string Enum = "global::System.Enum"; + public const string FromBase64String = "global::System.Convert.FromBase64String"; + public const string IConfiguration = "global::Microsoft.Extensions.Configuration.IConfiguration"; + public const string Int32 = "int"; + public const string IServiceCollection = "global::Microsoft.Extensions.DependencyInjection.IServiceCollection"; + public const string Object = "object"; + public const string String = "string"; + } + + private static class KeyWord + { + public const string @default = nameof(@default); + public const string @null = nameof(@null); + } + + private readonly SourceProductionContext _context; + private readonly SourceGenerationSpec _generationSpec; + + private readonly Queue _privateBindCoreMethodGen_QueuedTypes = new(); + + private readonly HashSet _internalBindMethodGen_ProcessedTypes = new(); + private readonly HashSet _privateBindCoreMethodGen_ProcessedTypes = new(); + + // Postfix for stringValueX variables used to save config value indexer + // results e.g. if (configuration["Key"] is string stringValue0) { ... } + private int _parseValueCount; + + private readonly SourceWriter _writer = new(); + + public Emitter(SourceProductionContext context, SourceGenerationSpec generationSpec) + { + _context = context; + _generationSpec = generationSpec; + } + + public void Emit() + { + _writer.WriteLine(@"// +#nullable enable annotations +#nullable disable warnings + +using System.Linq; +"); + + _writer.WriteBlockStart($"internal static class {Literal.GeneratedConfigurationBinder}"); + + EmitConfigureMethod(); + + EmitGetMethod(); + + EmitBindMethods(); + + _writer.WriteBlockEnd(); + + SourceText source = SourceText.From(_writer.GetSource(), Encoding.UTF8); + _context.AddSource($"{Literal.GeneratedConfigurationBinder}.g.cs", source); + } + + private void EmitConfigureMethod() + { + if (_generationSpec.TypesForConfigureMethodGen.Count == 0) + { + return; + } + + _writer.WriteBlockStart($"public static {GlobalName.IServiceCollection} {Literal.Configure}(this {GlobalName.IServiceCollection} {Literal.services}, {GlobalName.IConfiguration} {Literal.configuration})"); + + foreach (TypeSpec type in _generationSpec.TypesForConfigureMethodGen) + { + string typeDisplayString = type.DisplayString; + + _writer.WriteBlockStart($"if (typeof(T) == typeof({typeDisplayString}))"); + + _writer.WriteBlockStart($@"return {Literal.services}.{Literal.Configure}<{typeDisplayString}>({Literal.obj} =>"); + EmitBindLogicFromIConfiguration(type, Literal.obj, InitializationKind.None); + _writer.WriteBlockEnd(");"); + + _writer.WriteBlockEnd(); + } + + Emit_NotSupportedException_UnableToBindType(NotSupportedReason.TypeNotDetectedAsInput); + _writer.WriteBlockEnd(); + _writer.WriteBlankLine(); + } + + private void EmitGetMethod() + { + if (_generationSpec.TypesForGetMethodGen.Count == 0) + { + return; + } + + _writer.WriteBlockStart($"public static T? {Literal.Get}(this {GlobalName.IConfiguration} {Literal.configuration})"); + + EmitCheckForNullArgument(Literal.configuration); + + foreach (TypeSpec type in _generationSpec.TypesForGetMethodGen) + { + string typeDisplayString = type.DisplayString; + + _writer.WriteBlockStart($"if (typeof(T) == typeof({typeDisplayString}))"); + + EmitBindLogicFromIConfiguration(type, Literal.obj, InitializationKind.Declaration); + _writer.WriteLine($"return (T)({GlobalName.Object}){Literal.obj};"); + + _writer.WriteBlockEnd(); + _writer.WriteBlankLine(); + } + + Emit_NotSupportedException_UnableToBindType(NotSupportedReason.TypeNotDetectedAsInput); + _writer.WriteBlockEnd(); + _writer.WriteBlankLine(); + } + + private void EmitBindMethods() + { + if (_generationSpec.TypesForBindMethodGen.Count > 0) + { + foreach (TypeSpec type in _generationSpec.TypesForBindMethodGen) + { + EmitBindMethod(type); + } + } + + // Get & Configure method generation might have queued types for private BindCore impl + while (_privateBindCoreMethodGen_QueuedTypes.Count > 0) + { + EmitBindCoreMethod(_privateBindCoreMethodGen_QueuedTypes.Dequeue()); + } + } + + private void EmitBindMethod(TypeSpec type) + { + if (_internalBindMethodGen_ProcessedTypes.Contains(type)) + { + return; + } + + _internalBindMethodGen_ProcessedTypes.Add(type); + + // Binding to root level struct is a no-op. + // TODO: maybe this should be a debug assert & the parser shouldn't include them. + if (type.IsValueType) + { + return; + } + + _privateBindCoreMethodGen_QueuedTypes.Enqueue(type); + + _writer.WriteLine( + @$"internal static void {Literal.Bind}(this {GlobalName.IConfiguration} {Literal.configuration}, {type.DisplayString} {Literal.obj}) => " + + $"{Literal.BindCore}({Literal.configuration}, ref {Literal.obj});"); + _writer.WriteBlankLine(); + } + + private void EmitBindCoreMethod(TypeSpec type) + { + if (_privateBindCoreMethodGen_ProcessedTypes.Contains(type)) + { + return; + } + _privateBindCoreMethodGen_ProcessedTypes.Add(type); + + string objParameterExpression = $"ref {type.DisplayString} {Literal.obj}"; + _writer.WriteBlockStart(@$"private static void {Literal.BindCore}({GlobalName.IConfiguration} {Literal.configuration}, {objParameterExpression})"); + EmitBindCoreImpl(type); + _writer.WriteBlockEnd(); + _writer.WriteBlankLine(); + } + + private void EmitBindCoreImpl(TypeSpec type) + { + switch (type.SpecKind) + { + case TypeSpecKind.Array: + { + EmitBindCoreImplForArray((type as EnumerableSpec)!); + } + break; + case TypeSpecKind.IConfigurationSection: + { + EmitCastToIConfigurationSection(); + EmitAssignment(Literal.obj, Literal.section); + } + break; + case TypeSpecKind.Dictionary: + { + EmitBindCoreImplForDictionary((type as DictionarySpec)!); + } + break; + case TypeSpecKind.Enumerable: + { + EmitBindCoreImplForEnumerable((type as EnumerableSpec)!); + } + break; + case TypeSpecKind.Object: + { + EmitBindCoreImplForObject((type as ObjectSpec)!); + } + break; + case TypeSpecKind.Nullable: + { + EmitBindCoreImpl((type as NullableSpec)!.UnderlyingType); + } + break; + default: + Debug.Fail("Invalid type kind", type.SpecKind.ToString()); + break; + } + } + + private void EmitBindCoreImplForArray(EnumerableSpec type) + { + EnumerableSpec concreteType = (type.ConcreteType as EnumerableSpec)!; + Debug.Assert(type.SpecKind == TypeSpecKind.Array && type.ConcreteType is not null); + + EmitCheckForNullArgumentIfRequired(isValueType: false); + + string tempVarName = GetIncrementalVarName(Literal.temp); + + // Create and bind to temp list + EmitBindCoreCall(concreteType, tempVarName, Literal.configuration, InitializationKind.Declaration); + + // Resize array and copy fill with additional + EmitAssignment($"{GlobalName.Int32} {Literal.originalCount}", $"{Literal.obj}.{Literal.Length}"); + _writer.WriteLine($"{TypeFullName.Array}.{Literal.Resize}(ref {Literal.obj}, {Literal.originalCount} + {tempVarName}.{Literal.Count});"); + _writer.WriteLine($"{tempVarName}.{Literal.CopyTo}({Literal.obj}, {Literal.originalCount});"); + } + + private void EmitBindCoreImplForDictionary(DictionarySpec type) + { + EmitCheckForNullArgumentIfRequired(type.IsValueType); + + TypeSpec keyType = type.KeyType; + TypeSpec elementType = type.ElementType; + + EmitVarDeclaration(keyType, Literal.key); + + _writer.WriteBlockStart($"foreach ({TypeFullName.IConfigurationSection} {Literal.section} in {Literal.configuration}.{Literal.GetChildren}())"); + + // Parse key + EmitBindLogicFromString( + keyType, + Literal.key, + expressionForConfigStringValue: Expression.sectionKey, + writeExtraOnSuccess: Emit_BindAndAddLogic_ForElement); + + void Emit_BindAndAddLogic_ForElement() + { + // Validate not null for ref types + if (!keyType.IsValueType) { _writer.WriteBlockStart($"if ({Literal.key} is not {KeyWord.@null})"); } + + // For simple types: do regular dictionary add + if (elementType.SpecKind == TypeSpecKind.StringBasedParse) + { + EmitVarDeclaration(elementType, Literal.element); + EmitBindLogicFromIConfigurationSectionValue( + elementType, + Literal.element, + InitializationKind.SimpleAssignment, + writeExtraOnSuccess: () => EmitAssignment($"{Literal.obj}[{Literal.key}]", Literal.element)); + } + else // For complex types: + { + // If key already exists, bind to value to existing element instance if not null (for ref types) + string conditionToUseExistingElement = $"if ({Literal.obj}.{Literal.TryGetValue}({Literal.key}, out {elementType.DisplayString} {Literal.element})"; + conditionToUseExistingElement += !elementType.IsValueType + ? $" && {Literal.element} is not {KeyWord.@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, Literal.element, initKind); + EmitAssignment($"{Literal.obj}[{Literal.key}]", Literal.element); + } + } + + // End block for key null check + if (!keyType.IsValueType) { _writer.WriteBlockEnd(); } + } + + // End foreach loop. + _writer.WriteBlockEnd(); + } + + private void EmitBindCoreImplForEnumerable(EnumerableSpec type) + { + EmitCheckForNullArgumentIfRequired(type.IsValueType); + + TypeSpec elementType = type.ElementType; + + EmitVarDeclaration(elementType, Literal.element); + _writer.WriteBlockStart($"foreach ({TypeFullName.IConfigurationSection} {Literal.section} in {Literal.configuration}.{Literal.GetChildren}())"); + + EmitBindLogicFromIConfigurationSectionValue( + elementType, + Literal.element, + InitializationKind.SimpleAssignment, + writeExtraOnSuccess: EmitAddLogicForElement); + + void EmitAddLogicForElement() + { + string addExpression = $"{Literal.obj}.{Literal.Add}({Literal.element})"; + if (elementType.IsValueType) + { + _writer.WriteLine($"{addExpression};"); + } + else + { + _writer.WriteLine($"if ({Literal.element} is not {KeyWord.@null}) {{ {addExpression}; }}"); + } + } + + _writer.WriteBlockEnd(); + } + + private void EmitBindCoreImplForObject(ObjectSpec type) + { + EmitCheckForNullArgumentIfRequired(type.IsValueType); + + foreach (PropertySpec property in type.Properties) + { + TypeSpec propertyType = property.Type; + + EmitBindCoreImplForProperty(property, propertyType, parentType: type); + } + } + + private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propertyType, TypeSpec parentType) + { + string configurationKeyName = property.ConfigurationKeyName; + string propertyParentReference = property.IsStatic ? parentType.DisplayString : Literal.obj; + string expressionForPropertyAccess = $"{propertyParentReference}.{property.Name}"; + string expressionForConfigGetSection = $@"{Literal.configuration}.{Literal.GetSection}(""{configurationKeyName}"")"; + string expressionForConfigSectionValue = $"{expressionForConfigGetSection}.{Literal.Value}"; + + bool canGet = property.CanGet; + bool canSet = property.CanSet; + + switch (propertyType.SpecKind) + { + case TypeSpecKind.System_Object: + { + EmitAssignment(expressionForPropertyAccess, expressionForConfigSectionValue); + } + break; + case TypeSpecKind.StringBasedParse: + case TypeSpecKind.ByteArray: + { + if (canSet) + { + EmitBindLogicFromString( + propertyType, + expressionForPropertyAccess, + expressionForConfigSectionValue); + } + } + break; + case TypeSpecKind.Array: + { + EmitBindCoreCallForProperty( + property, + propertyType, + expressionForPropertyAccess, + expressionForConfigArg: expressionForConfigGetSection); + } + break; + case TypeSpecKind.IConfigurationSection: + { + EmitAssignment(expressionForPropertyAccess, expressionForConfigGetSection); + } + break; + case TypeSpecKind.Nullable: + { + TypeSpec underlyingType = (propertyType as NullableSpec)!.UnderlyingType; + EmitBindCoreImplForProperty(property, underlyingType, parentType); + } + break; + default: + { + EmitBindCoreCallForProperty( + property, + propertyType, + expressionForPropertyAccess, + expressionForConfigArg: expressionForConfigGetSection); + } + break; + } + } + + private void EmitBindLogicFromIConfiguration(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind) + { + if (type.SpecKind is TypeSpecKind.StringBasedParse or TypeSpecKind.ByteArray) + { + EmitCastToIConfigurationSection(); + if (initKind is InitializationKind.Declaration) + { + EmitAssignment($"{type.DisplayString} {expressionForMemberAccess}", KeyWord.@default); + } + EmitBindLogicFromString(type, expressionForMemberAccess, Expression.sectionValue); + } + else + { + if (initKind is InitializationKind.Declaration) + { + EmitAssignment($"{TypeFullName.IConfigurationSection}? {Literal.section}", $"{Literal.configuration} as {TypeFullName.IConfigurationSection}"); + _writer.WriteBlockStart($"if ({Expression.sectionValue} is null && !{Literal.configuration}.{Literal.GetChildren}().{Literal.Any}())"); + _writer.WriteLine($"return {KeyWord.@default};"); + _writer.WriteBlockEnd(); + } + EmitBindCoreCall(type, expressionForMemberAccess, Literal.configuration, initKind); + } + } + + 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, Literal.section, initKind); + writeExtraOnSuccess?.Invoke(); + } + } + + private void EmitBindCoreCall( + TypeSpec type, + string expressionForMemberAccess, + string expressionForConfigArg, + InitializationKind initKind) + { + string tempVarName = GetIncrementalVarName(Literal.temp); + if (initKind is InitializationKind.AssignmentWithNullCheck) + { + EmitAssignment($"{type.DisplayString} {tempVarName}", $"{expressionForMemberAccess}"); + EmitObjectInit(type, tempVarName, InitializationKind.AssignmentWithNullCheck); + _writer.WriteLine($@"{Literal.BindCore}({expressionForConfigArg}, ref {tempVarName});"); + } + else if (initKind is InitializationKind.None && type.IsValueType) + { + EmitObjectInit(type, tempVarName, InitializationKind.Declaration); + _writer.WriteLine($@"{Literal.BindCore}({expressionForConfigArg}, ref {tempVarName});"); + EmitAssignment(expressionForMemberAccess, tempVarName); + } + else + { + EmitObjectInit(type, expressionForMemberAccess, initKind); + _writer.WriteLine($@"{Literal.BindCore}({expressionForConfigArg}, ref {expressionForMemberAccess});"); + } + + _privateBindCoreMethodGen_QueuedTypes.Enqueue(type); + } + + private void EmitBindCoreCallForProperty( + PropertySpec property, + TypeSpec effectivePropertyType, + string expressionForPropertyAccess, + string expressionForConfigArg) + { + bool canGet = property.CanGet; + bool canSet = property.CanSet; + + string tempVarName = GetIncrementalVarName(Literal.temp); + if (effectivePropertyType.IsValueType) + { + if (canSet) + { + if (canGet) + { + TypeSpec actualPropertyType = property.Type; + if (actualPropertyType.SpecKind is TypeSpecKind.Nullable) + { + string nullableTempVarName = GetIncrementalVarName(Literal.temp); + EmitAssignment( + $"{actualPropertyType.DisplayString} {nullableTempVarName}", expressionForPropertyAccess); + EmitAssignment( + $"{effectivePropertyType.DisplayString} {tempVarName}", + $"{nullableTempVarName}.{Literal.HasValue} ? {nullableTempVarName}.{Literal.Value} : new {effectivePropertyType.DisplayString}()"); + } + else + { + EmitAssignment($"{effectivePropertyType.DisplayString} {tempVarName}", $"{expressionForPropertyAccess}"); + } + } + else + { + EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.Declaration); + } + + _writer.WriteLine($@"{Literal.BindCore}({expressionForConfigArg}, ref {tempVarName});"); + EmitAssignment(expressionForPropertyAccess, tempVarName); + _privateBindCoreMethodGen_QueuedTypes.Enqueue(effectivePropertyType); + } + } + else if (canGet) + { + EmitAssignment($"{effectivePropertyType.DisplayString} {tempVarName}", $"{expressionForPropertyAccess}"); + EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.AssignmentWithNullCheck); + _writer.WriteLine($@"{Literal.BindCore}({expressionForConfigArg}, ref {tempVarName});"); + + if (canSet) + { + EmitAssignment(expressionForPropertyAccess, tempVarName); + } + } + else + { + Debug.Assert(canSet); + EmitObjectInit(effectivePropertyType, tempVarName, InitializationKind.Declaration); + _writer.WriteLine($@"{Literal.BindCore}({expressionForConfigArg}, ref {tempVarName});"); + EmitAssignment(expressionForPropertyAccess, tempVarName); + } + + _privateBindCoreMethodGen_QueuedTypes.Enqueue(effectivePropertyType); + } + + private void EmitBindLogicFromString( + TypeSpec type, + string expressionForMemberAccess, + string expressionForConfigStringValue, + Action? writeExtraOnSuccess = null) + { + string typeDisplayString = type.DisplayString; + string stringValueVarName = GetIncrementalVarName(Literal.stringValue); + string assignmentCondition = $"{expressionForConfigStringValue} is {GlobalName.String} {stringValueVarName}"; + + string rhs; + if (type.SpecialType != SpecialType.None) + { + rhs = type.SpecialType switch + { + SpecialType.System_String => stringValueVarName, + SpecialType.System_Object => KeyWord.@default, + _ => $"{typeDisplayString}.{Literal.Parse}({stringValueVarName})" + }; + } + else if (type.SpecKind == TypeSpecKind.Enum) + { + string enumValueVarName = GetIncrementalVarName(Literal.enumValue); + assignmentCondition += $" && {GlobalName.Enum}.{Literal.TryParse}({stringValueVarName}, true, out {typeDisplayString} {enumValueVarName})"; + rhs = enumValueVarName; + } + else if (type.SpecKind == TypeSpecKind.ByteArray) + { + rhs = $"{GlobalName.FromBase64String}({stringValueVarName})"; + } + else + { + return; + } + + if (writeExtraOnSuccess is null) + { + _writer.WriteLine($"if ({assignmentCondition}) {{ {expressionForMemberAccess} = {rhs}; }}"); + } + else + { + _writer.WriteBlockStart($"if ({assignmentCondition})"); + EmitAssignment(expressionForMemberAccess, rhs); + writeExtraOnSuccess(); + _writer.WriteBlockEnd(); + } + } + + private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind) + { + if (initKind is InitializationKind.None or InitializationKind.None) + { + return; + } + + string displayString = type.DisplayString; + string expressionForInit = null; + if (type is EnumerableSpec { SpecKind: TypeSpecKind.Array } arrayType) + { + Regex regex = new(Regex.Escape("[]")); + expressionForInit = $"new {regex.Replace(type.DisplayString, "[0]", 1)};"; + } + else if (type.ConstructionStrategy != ConstructionStrategy.ParameterlessConstructor) + { + return; + } + else if (type is CollectionSpec { ConcreteType: { } concreteType}) + { + displayString = concreteType.DisplayString; + } + + // Not an array. + expressionForInit ??= $"new {displayString}()"; + + if (initKind == InitializationKind.Declaration) + { + Debug.Assert(!expressionForMemberAccess.Contains(".")); + EmitAssignment($"{displayString} {expressionForMemberAccess}", expressionForInit); + } + else if (initKind == InitializationKind.AssignmentWithNullCheck) + { + _writer.WriteLine($"{expressionForMemberAccess} ??= {expressionForInit};"); + } + else + { + EmitAssignment(expressionForMemberAccess, expressionForInit); + } + } + + private void EmitCastToIConfigurationSection() + { + _writer.WriteBlockStart($"if ({Literal.configuration} is not {TypeFullName.IConfigurationSection} {Literal.section})"); + _writer.WriteLine("throw new global::System.InvalidOperationException();"); + _writer.WriteBlockEnd(); + } + + private void EmitVarDeclaration(TypeSpec type, string varName) => _writer.WriteLine($"{type.DisplayString} {varName};"); + + private void EmitAssignment(string lhsSource, string rhsSource) => _writer.WriteLine($"{lhsSource} = {rhsSource};"); + + private void Emit_NotSupportedException_UnableToBindType(string reason, string typeDisplayString = "{typeof(T)}") => + _writer.WriteLine(@$"throw new global::System.NotSupportedException($""{string.Format(ExceptionMessages.TypeNotSupported, typeDisplayString, reason)}"");"); + + private void EmitCheckForNullArgumentIfRequired(bool isValueType) + { + if (!isValueType) + { + EmitCheckForNullArgument(Literal.obj); + } + } + + private void EmitCheckForNullArgument(string argName) + { + _writer.WriteBlockStart($"if ({argName} is {KeyWord.@null})"); + _writer.WriteLine($"throw new global::System.ArgumentNullException(nameof({argName}));"); + _writer.WriteBlockEnd(); + } + + private string GetIncrementalVarName(string prefix) => $"{prefix}{_parseValueCount++}"; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs new file mode 100644 index 0000000000000..f0b5e3a3e35dc --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs @@ -0,0 +1,149 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + public sealed partial class ConfigurationBindingSourceGenerator + { + private const string GeneratorProjectName = "Microsoft.Extensions.Configuration.Binder.SourceGeneration"; + + private static DiagnosticDescriptor TypeNotSupported { get; } = new DiagnosticDescriptor( + id: "SYSLIB1100", + title: new LocalizableResourceString(nameof(SR.TypeNotSupportedTitle), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Configuration.Binder.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.TypeNotSupportedMessageFormat), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Configuration.Binder.SourceGeneration.SR)), + category: GeneratorProjectName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); + + private static DiagnosticDescriptor PropertyNotSupported { get; } = new DiagnosticDescriptor( + id: "SYSLIB1101", + title: new LocalizableResourceString(nameof(SR.PropertyNotSupportedTitle), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Configuration.Binder.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.PropertyNotSupportedMessageFormat), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Configuration.Binder.SourceGeneration.SR)), + category: GeneratorProjectName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); + + // Unlike sourcegen warnings, exception messages should not be localized so we keep them in source. + private static class ExceptionMessages + { + public const string TypeNotSupported = "Unable to bind to type '{0}': '{1}'"; + } + + private static class Literal + { + public const string configuration = nameof(configuration); + public const string element = nameof(element); + public const string enumValue = nameof(enumValue); + public const string key = nameof(key); + public const string obj = nameof(obj); + public const string originalCount = nameof(originalCount); + public const string path = nameof(path); + public const string section = nameof(section); + public const string services = nameof(services); + public const string stringValue = nameof(stringValue); + public const string temp = nameof(temp); + + public const string Add = nameof(Add); + public const string Any = nameof(Any); + public const string Bind = nameof(Bind); + public const string BindCore = nameof(BindCore); + public const string Configure = nameof(Configure); + public const string CopyTo = nameof(CopyTo); + public const string ContainsKey = nameof(ContainsKey); + public const string Count = nameof(Count); + public const string GeneratedConfigurationBinder = nameof(GeneratedConfigurationBinder); + public const string Get = nameof(Get); + public const string GetChildren = nameof(GetChildren); + public const string GetSection = nameof(GetSection); + public const string HasValue = nameof(HasValue); + public const string Length = nameof(Length); + public const string Parse = nameof(Parse); + public const string Resize = nameof(Resize); + public const string TryGetValue = nameof(TryGetValue); + public const string TryParse = nameof(TryParse); + 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 KeyTypeNotSupported = "The collection key 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 Array = "System.Array"; + public const string ConfigurationKeyNameAttribute = "Microsoft.Extensions.Configuration.ConfigurationKeyNameAttribute"; + public const string Dictionary = "System.Collections.Generic.Dictionary`2"; + public const string GenericIDictionary = "System.Collections.Generic.IDictionary`2"; + public const string HashSet = "System.Collections.Generic.HashSet`1"; + public const string ISet = "System.Collections.Generic.ISet`1"; + public const string IConfigurationSection = "Microsoft.Extensions.Configuration.IConfigurationSection"; + public const string IConfiguration = "Microsoft.Extensions.Configuration.IConfiguration"; + public const string IDictionary = "System.Collections.Generic.IDictionary"; + public const string IServiceCollection = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; + public const string List = "System.Collections.Generic.List`1"; + } + + private static bool TypesAreEqual(ITypeSymbol first, ITypeSymbol second) + => first.Equals(second, SymbolEqualityComparer.Default); + + private enum InitializationKind + { + None = 0, + SimpleAssignment = 1, + AssignmentWithNullCheck = 2, + Declaration = 3, + } + + private sealed class SourceWriter + { + private readonly StringBuilder _sb = new(); + private int _indentationLevel; + + public int Length => _sb.Length; + public int IndentationLevel => _indentationLevel; + + public void WriteBlockStart(string declaration) + { + WriteLine(declaration); + WriteLine("{"); + _indentationLevel++; + } + + public void WriteBlockEnd(string? extra = null) + { + _indentationLevel--; + Debug.Assert(_indentationLevel > -1); + WriteLine($"}}{extra}"); + } + + public void WriteLine(string source) + { + string indentationSource = _indentationLevel == 0 + ? "" + : string.Join("", Enumerable.Repeat(" ", 4 * _indentationLevel)); + + _sb.AppendLine($"{indentationSource}{source}"); + } + + public void WriteBlankLine() => _sb.AppendLine(); + + public string GetSource() => _sb.ToString(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs new file mode 100644 index 0000000000000..d7ac9cd385b38 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs @@ -0,0 +1,655 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.DotnetRuntime.Extensions; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.Extensions.Configuration.Binder.SourceGeneration; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + public sealed partial class ConfigurationBindingSourceGenerator + { + private sealed class Parser + { + private readonly Compilation _compilation; + private readonly SourceProductionContext _context; + + private readonly INamedTypeSymbol _symbolForGenericIList; + private readonly INamedTypeSymbol _symbolForICollection; + private readonly INamedTypeSymbol _symbolForIEnumerable; + private readonly INamedTypeSymbol _symbolForString; + + private readonly INamedTypeSymbol? _symbolForConfigurationKeyNameAttribute; + private readonly INamedTypeSymbol? _symbolForDictionary; + private readonly INamedTypeSymbol? _symbolForGenericIDictionary; + private readonly INamedTypeSymbol? _symbolForHashSet; + private readonly INamedTypeSymbol? _symbolForIConfiguration; + private readonly INamedTypeSymbol? _symbolForIConfigurationSection; + private readonly INamedTypeSymbol? _symbolForIDictionary; + private readonly INamedTypeSymbol? _symbolForIServiceCollection; + private readonly INamedTypeSymbol? _symbolForISet; + private readonly INamedTypeSymbol? _symbolForList; + + private readonly HashSet _typesForBindMethodGen = new(); + private readonly HashSet _typesForGetMethodGen = new(); + private readonly HashSet _typesForConfigureMethodGen = new(); + +#pragma warning disable RS1024 + private readonly HashSet _unsupportedTypes = new(SymbolEqualityComparer.Default); + private readonly Dictionary _createdSpecs = new(SymbolEqualityComparer.Default); +#pragma warning restore RS1024 + + public Parser(SourceProductionContext context, Compilation compilation) + { + _compilation = compilation; + _context = context; + + _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); + } + + public SourceGenerationSpec? GetSourceGenerationSpec( + IEnumerable invocations, + CancellationToken cancellationToken) + { + if (_symbolForIConfiguration is null || _symbolForIServiceCollection is null) + { + return null; + } + + foreach (InvocationExpressionSyntax invocation in invocations) + { + if (IsBindCall(invocation)) + { + ProcessBindCall(invocation, cancellationToken); + } + else if (IsGetCall(invocation)) + { + ProcessGetCall(invocation, cancellationToken); + } + else if (IsConfigureCall(invocation)) + { + ProcessConfigureCall(invocation, cancellationToken); + } + } + + return new SourceGenerationSpec(_typesForBindMethodGen, _typesForGetMethodGen, _typesForConfigureMethodGen); + } + + public static bool IsInputCall(SyntaxNode node) => + node is not InvocationExpressionSyntax invocation + ? false + : IsBindCall(invocation) || IsConfigureCall(invocation) || IsGetCall(invocation); + + private void ProcessBindCall(InvocationExpressionSyntax invocation, CancellationToken cancellationToken) + { + SemanticModel semanticModel = _compilation.GetSemanticModel(invocation.SyntaxTree); + IInvocationOperation operation = semanticModel.GetOperation(invocation, cancellationToken) as IInvocationOperation; + + // We're looking for IConfiguration.Bind(object). + if (operation is IInvocationOperation { Arguments: { Length: 2 } arguments } && + operation.TargetMethod.IsExtensionMethod && + TypesAreEqual(_symbolForIConfiguration, arguments[0].Parameter.Type) && + arguments[1].Parameter.Type.SpecialType == SpecialType.System_Object) + { + IConversionOperation argument = arguments[1].Value as IConversionOperation; + ITypeSymbol? type = ResolveType(argument)?.WithNullableAnnotation(NullableAnnotation.None); + + // TODO: do we need diagnostic for System.Object? + if (type is not INamedTypeSymbol { } namedType || + namedType.SpecialType == SpecialType.System_Object || + namedType.SpecialType == SpecialType.System_Void) + { + return; + } + + AddTargetConfigType(_typesForBindMethodGen, namedType, invocation.GetLocation()); + + static ITypeSymbol? ResolveType(IOperation argument) => + argument switch + { + IConversionOperation c => ResolveType(c.Operand), + IInstanceReferenceOperation i => i.Type, + ILocalReferenceOperation l => l.Local.Type, + IFieldReferenceOperation f => f.Field.Type, + IMethodReferenceOperation m when m.Method.MethodKind == MethodKind.Constructor => m.Method.ContainingType, + IMethodReferenceOperation m => m.Method.ReturnType, + IAnonymousFunctionOperation f => f.Symbol.ReturnType, + _ => null + }; + } + } + + private void ProcessGetCall(InvocationExpressionSyntax invocation, CancellationToken cancellationToken) + { + SemanticModel semanticModel = _compilation.GetSemanticModel(invocation.SyntaxTree); + IInvocationOperation? operation = semanticModel.GetOperation(invocation, cancellationToken) as IInvocationOperation; + + // We're looking for IConfiguration.Get(). + if (operation is IInvocationOperation { Arguments.Length: 1 } invocationOperation && + invocationOperation.TargetMethod.IsExtensionMethod && + invocationOperation.TargetMethod.IsGenericMethod && + TypesAreEqual(_symbolForIConfiguration, invocationOperation.TargetMethod.Parameters[0].Type)) + { + ITypeSymbol? type = invocationOperation.TargetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None); + if (type is not INamedTypeSymbol { } namedType || + namedType.SpecialType == SpecialType.System_Object || + namedType.SpecialType == SpecialType.System_Void) + { + return; + } + + AddTargetConfigType(_typesForGetMethodGen, namedType, invocation.GetLocation()); + } + } + + private void ProcessConfigureCall(InvocationExpressionSyntax invocation, CancellationToken cancellationToken) + { + SemanticModel semanticModel = _compilation.GetSemanticModel(invocation.SyntaxTree); + IOperation? operation = semanticModel.GetOperation(invocation, cancellationToken); + + // We're looking for IServiceCollection.Configure(IConfiguration). + if (operation is IInvocationOperation { Arguments.Length: 2 } invocationOperation && + invocationOperation.TargetMethod.IsExtensionMethod && + invocationOperation.TargetMethod.IsGenericMethod && + TypesAreEqual(_symbolForIServiceCollection, invocationOperation.TargetMethod.Parameters[0].Type) && + TypesAreEqual(_symbolForIConfiguration, invocationOperation.TargetMethod.Parameters[1].Type)) + { + ITypeSymbol? type = invocationOperation.TargetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None); + if (type is not INamedTypeSymbol { } namedType || + namedType.SpecialType == SpecialType.System_Object) + { + return; + } + + AddTargetConfigType(_typesForConfigureMethodGen, namedType, invocation.GetLocation()); + } + } + + public static bool IsBindCall(SyntaxNode node) => + node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: IdentifierNameSyntax + { + Identifier.ValueText: "Bind" + } + }, + ArgumentList.Arguments.Count: 1 + }; + + public static bool IsConfigureCall(SyntaxNode node) => + node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + Identifier.ValueText: "Configure" + } + }, + ArgumentList.Arguments.Count: 1 + }; + + public static bool IsGetCall(SyntaxNode node) => + node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + Identifier.ValueText: "Get" + } + }, + ArgumentList.Arguments.Count: 0 + }; + + private TypeSpec? AddTargetConfigType(HashSet specs, ITypeSymbol type, Location? location) + { + if (type is not INamedTypeSymbol namedType || ContainsGenericParameters(namedType)) + { + return null; + } + + TypeSpec? spec = GetOrCreateTypeSpec(namedType, location); + if (spec != null && !specs.Contains(spec)) + { + specs.Add(spec); + } + + return spec; + } + + private TypeSpec? GetOrCreateTypeSpec(ITypeSymbol type, Location? location = null) + { + if (_createdSpecs.TryGetValue(type, out TypeSpec? spec)) + { + return spec; + } + + if (type.Name == "IDictionary" && type is INamedTypeSymbol { IsGenericType: false }) + { + } + + 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 && + genericType.ConstructUnboundGenericType() is INamedTypeSymbol { } unboundGeneric && + unboundGeneric.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + return TryGetTypeSpec(genericType.TypeArguments[0], NotSupportedReason.NullableUnderlyingTypeNotSupported, out TypeSpec? underlyingType) + ? CacheSpec(new NullableSpec(type) { Location = location, UnderlyingType = underlyingType }) + : null; + } + else if (type.SpecialType != SpecialType.None) + { + return CacheSpec(new TypeSpec(type) { Location = location }); + } + else if (IsEnum(type)) + { + return CacheSpec(new TypeSpec(type) { Location = location, SpecKind = TypeSpecKind.Enum }); + } + else if (type is IArrayTypeSymbol { } arrayType) + { + spec = CreateArraySpec(arrayType, location); + return spec == null ? null : CacheSpec(spec); + } + else if (TypesAreEqual(type, _symbolForIConfigurationSection)) + { + return CacheSpec(new TypeSpec(type) { Location = location, SpecKind = TypeSpecKind.IConfigurationSection }); + } + else if (type is INamedTypeSymbol namedType) + { + return IsCollection(namedType) + ? CacheSpec(CreateCollectionSpec(namedType, location)) + : CacheSpec(CreateObjectSpec(namedType, location)); + } + + ReportUnsupportedType(type, NotSupportedReason.TypeNotSupported, location); + return null; + + T CacheSpec(T? s) where T : TypeSpec + { + _createdSpecs[type] = s; + return s; + } + } + + private bool TryGetTypeSpec(ITypeSymbol type, string unsupportedReason, out TypeSpec? spec) + { + spec = GetOrCreateTypeSpec(type); + + if (spec == null) + { + ReportUnsupportedType(type, unsupportedReason); + return false; + } + + return true; + } + + private EnumerableSpec? CreateArraySpec(IArrayTypeSymbol arrayType, Location? location) + { + if (arrayType.Rank > 1) + { + ReportUnsupportedType(arrayType, NotSupportedReason.MultiDimArraysNotSupported, location); + return null; + } + + if (!TryGetTypeSpec(arrayType.ElementType, NotSupportedReason.ElementTypeNotSupported, out TypeSpec? elementSpec)) + { + return null; + } + + EnumerableSpec spec; + if (elementSpec.SpecialType is SpecialType.System_Byte) + { + spec = new EnumerableSpec(arrayType) { Location = location, SpecKind = TypeSpecKind.ByteArray, ElementType = elementSpec }; + } + else + { + // We want a Bind method for List as a temp holder for the array values. + EnumerableSpec? listSpec = ConstructAndCacheGenericTypeForBind(_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, + }; + } + + return spec; + } + + private CollectionSpec? CreateCollectionSpec(INamedTypeSymbol type, Location? location) + { + if (IsCandidateDictionary(type, out ITypeSymbol keyType, out ITypeSymbol elementType)) + { + return CreateDictionarySpec(type, location, keyType, elementType); + } + else if (IsCandidateEnumerable(type, out elementType)) + { + return CreateEnumerableSpec(type, location, elementType); + } + + return null; + } + + private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? location, ITypeSymbol keyType, ITypeSymbol elementType) + { + if (!TryGetTypeSpec(keyType, NotSupportedReason.KeyTypeNotSupported, out TypeSpec keySpec) || + !TryGetTypeSpec(elementType, NotSupportedReason.ElementTypeNotSupported, out TypeSpec elementSpec)) + { + return null; + } + + if (keySpec.SpecKind != TypeSpecKind.StringBasedParse) + { + ReportUnsupportedType(type, NotSupportedReason.DictionaryKeyNotSupported, location); + return null; + } + + DictionarySpec? concreteType = null; + if (IsInterfaceMatch(type, _symbolForGenericIDictionary) || IsInterfaceMatch(type, _symbolForIDictionary)) + { + // We know the key and element types are supported. + concreteType = ConstructAndCacheGenericTypeForBind(_symbolForDictionary, keyType, elementType) as DictionarySpec; + Debug.Assert(concreteType != null); + } + else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType, keyType)) + { + ReportUnsupportedType(type, NotSupportedReason.CollectionNotSupported, location); + return null; + } + + return new DictionarySpec(type) + { + Location = location, + KeyType = keySpec, + ElementType = elementSpec, + ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor, + ConcreteType = concreteType + }; + } + + private TypeSpec? ConstructAndCacheGenericTypeForBind(INamedTypeSymbol type, params ITypeSymbol[] parameters) + { + Debug.Assert(type.IsGenericType); + return AddTargetConfigType(_typesForBindMethodGen, type.Construct(parameters), location: null); + } + + private EnumerableSpec? CreateEnumerableSpec(INamedTypeSymbol type, Location? location, ITypeSymbol elementType) + { + if (!TryGetTypeSpec(elementType, NotSupportedReason.ElementTypeNotSupported, out TypeSpec elementSpec)) + { + return null; + } + + EnumerableSpec? concreteType = null; + if (IsInterfaceMatch(type, _symbolForISet)) + { + concreteType = ConstructAndCacheGenericTypeForBind(_symbolForHashSet, elementType) as EnumerableSpec; + } + else if (IsInterfaceMatch(type, _symbolForICollection) || + IsInterfaceMatch(type, _symbolForGenericIList)) + { + concreteType = ConstructAndCacheGenericTypeForBind(_symbolForList, elementType) as EnumerableSpec; + } + else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType)) + { + ReportUnsupportedType(type, NotSupportedReason.CollectionNotSupported, location); + return null; + } + + return new EnumerableSpec(type) + { + Location = location, + ElementType = elementSpec, + ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor, + ConcreteType = concreteType + }; + } + + private ObjectSpec? CreateObjectSpec(INamedTypeSymbol type, Location? location) + { + if (!CanConstructObject(type, location)) + { + return null; + } + + List properties = new(); + INamedTypeSymbol current = type; + while (current != null) + { + foreach (ISymbol member in current.GetMembers()) + { + if (member is IPropertySymbol { IsIndexer: false } property) + { + if (property.Type is ITypeSymbol { } propertyType) + { + TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(propertyType); + string propertyName = property.Name; + + if (propertyTypeSpec is null) + { + _context.ReportDiagnostic(Diagnostic.Create(PropertyNotSupported, location, new string[] { propertyName, type.ToDisplayString() })); + } + else + { + AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => TypesAreEqual(a.AttributeClass, _symbolForConfigurationKeyNameAttribute)); + string? configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName; + + PropertySpec spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; + if (spec.CanGet || spec.CanSet) + { + properties.Add(spec); + } + } + } + } + } + current = current.BaseType; + } + + return new ObjectSpec(type) { Location = location, Properties = properties, ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor }; + } + + private bool IsCandidateEnumerable(INamedTypeSymbol type, out ITypeSymbol? elementType) + { + INamedTypeSymbol? @interface = GetInterface(type, _symbolForICollection); + + if (@interface is not null) + { + elementType = @interface.TypeArguments[0]; + return true; + } + + elementType = null; + return false; + } + + private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyType, out ITypeSymbol? elementType) + { + INamedTypeSymbol? @interface = GetInterface(type, _symbolForGenericIDictionary); + if (@interface is not null) + { + keyType = @interface.TypeArguments[0]; + elementType = @interface.TypeArguments[1]; + return true; + } + + if (IsInterfaceMatch(type, _symbolForIDictionary)) + { + keyType = _symbolForString; + elementType = _symbolForString; + return true; + } + + keyType = null; + elementType = null; + return false; + } + + private bool IsCollection(INamedTypeSymbol type) => + GetInterface(type, _symbolForIEnumerable) is not null; + + private static INamedTypeSymbol? GetInterface(INamedTypeSymbol type, INamedTypeSymbol @interface) + { + if (IsInterfaceMatch(type, @interface)) + { + return type; + } + + if (@interface.IsGenericType) + { + return type.AllInterfaces.FirstOrDefault(candidate => + candidate.IsGenericType && + candidate.ConstructUnboundGenericType() is INamedTypeSymbol unbound + && TypesAreEqual(unbound, @interface)); + } + + return type.AllInterfaces.FirstOrDefault(candidate => TypesAreEqual(candidate, @interface)); + } + + private static bool IsInterfaceMatch(INamedTypeSymbol type, INamedTypeSymbol @interface) + { + if (type.IsGenericType) + { + INamedTypeSymbol unbound = type.ConstructUnboundGenericType(); + return TypesAreEqual(unbound, @interface); + } + + return TypesAreEqual(type, @interface); + } + + public static bool ContainsGenericParameters(INamedTypeSymbol type) + { + if (!type.IsGenericType) + { + return false; + } + + foreach (ITypeSymbol typeArg in type.TypeArguments) + { + if (typeArg.TypeKind == TypeKind.TypeParameter) + { + return true; + } + } + + return false; + } + + private bool CanConstructObject(INamedTypeSymbol type, Location? location) + { + if (type.IsAbstract || type.TypeKind == TypeKind.Interface) + { + ReportUnsupportedType(type, NotSupportedReason.AbstractOrInterfaceNotSupported, location); + return false; + } + else if (!HasPublicParameterlessCtor(type)) + { + ReportUnsupportedType(type, NotSupportedReason.NeedPublicParameterlessConstructor, location); + return false; + } + + return true; + } + + private static bool HasPublicParameterlessCtor(ITypeSymbol type) + { + if (type is not INamedTypeSymbol namedType) + { + return false; + } + + foreach (IMethodSymbol ctor in namedType.InstanceConstructors) + { + if (ctor.DeclaredAccessibility == Accessibility.Public && ctor.Parameters.Length == 0) + { + return true; + } + } + + return false; + } + + private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol element) + { + INamedTypeSymbol current = type; + while (current != null) + { + if (current.GetMembers(Literal.Add).Any(member => + member is IMethodSymbol { Parameters.Length: 1 } method && + TypesAreEqual(element, method.Parameters[0].Type))) + { + return true; + } + current = current.BaseType; + } + return false; + } + + private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol element, ITypeSymbol key) + { + INamedTypeSymbol current = type; + while (current != null) + { + if (current.GetMembers(Literal.Add).Any(member => + member is IMethodSymbol { Parameters.Length: 2 } method && + TypesAreEqual(key, method.Parameters[0].Type) && + TypesAreEqual(element, method.Parameters[1].Type))) + { + return true; + } + current = current.BaseType; + } + return false; + } + + private static bool IsEnum(ITypeSymbol type) => type is INamedTypeSymbol { EnumUnderlyingType: INamedTypeSymbol { } }; + + private void ReportUnsupportedType(ITypeSymbol type, string reason, Location? location = null) + { + if (!_unsupportedTypes.Contains(type)) + { + _context.ReportDiagnostic( + Diagnostic.Create(TypeNotSupported, location, new string[] { type.ToDisplayString(), reason })); + _unsupportedTypes.Add(type); + } + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs new file mode 100644 index 0000000000000..ddf5afaeeacb1 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//#define LAUNCH_DEBUGGER +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + /// + /// Generates source code to optimize binding with ConfigurationBinder. + /// + [Generator] + public sealed partial class ConfigurationBindingSourceGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider inputCalls = context.SyntaxProvider.CreateSyntaxProvider( + (node, _) => Parser.IsInputCall(node), + (syntaxContext, _) => (InvocationExpressionSyntax)syntaxContext.Node); + + IncrementalValueProvider<(Compilation, ImmutableArray)> compilationAndClasses = + context.CompilationProvider.Combine(inputCalls.Collect()); + + context.RegisterSourceOutput(compilationAndClasses, (spc, source) => Execute(source.Item1, source.Item2, spc)); + } + + /// + /// Generates source code to optimize binding with ConfigurationBinder. + /// + private static void Execute(Compilation compilation, ImmutableArray inputCalls, SourceProductionContext context) + { +#if LAUNCH_DEBUGGER + #pragma warning disable IDE0055 + if (!System.Diagnostics.Debugger.IsAttached) + { + System.Diagnostics.Debugger.Launch(); + } + try + { +#endif + if (inputCalls.IsDefaultOrEmpty) + { + return; + } + + Parser parser = new(context, compilation); + SourceGenerationSpec? spec = parser.GetSourceGenerationSpec(inputCalls, context.CancellationToken); + if (spec is not null) + { + Emitter emitter = new(context, spec); + emitter.Emit(); + } +#if LAUNCH_DEBUGGER + } + catch (System.Exception ex) + { + System.Diagnostics.Debugger.Break(); + throw ex; + } + #pragma warning restore +#endif + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConstructionStrategy.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConstructionStrategy.cs new file mode 100644 index 0000000000000..21db02547258a --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConstructionStrategy.cs @@ -0,0 +1,11 @@ +// 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 +{ + internal enum ConstructionStrategy + { + NotApplicable = 0, + 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 new file mode 100644 index 0000000000000..d3604788bea13 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj @@ -0,0 +1,50 @@ + + + netstandard2.0 + $(MSBuildThisFileName) + $(MSBuildThisFileName) + SR + FxResources.$(RootNamespace).$(StringResourcesClassName) + false + + CS1574 + false + true + cs + 4.4 + $(MicrosoftCodeAnalysisVersion_4_4) + $(DefineConstants);ROSLYN4_0_OR_GREATER;ROSLYN4_4_OR_GREATER + + + + $(DefineConstants);BUILDING_SOURCE_GENERATOR + $(DefineConstants);LAUNCH_DEBUGGER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/NullableSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/NullableSpec.cs new file mode 100644 index 0000000000000..69ad69cdbfdd8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/NullableSpec.cs @@ -0,0 +1,15 @@ +// 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 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/ObjectSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ObjectSpec.cs new file mode 100644 index 0000000000000..752654f25e9ec --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ObjectSpec.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + internal sealed record ObjectSpec : TypeSpec + { + public ObjectSpec(INamedTypeSymbol type) : base(type) { } + public override TypeSpecKind SpecKind => TypeSpecKind.Object; + public required List Properties { get; init; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/PopulationStrategy.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/PopulationStrategy.cs new file mode 100644 index 0000000000000..05ba9e9cd0e94 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/PopulationStrategy.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. + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + internal enum PopulationStrategy + { + NotApplicable = 0, + Indexer = 1, + Add = 2, + Push = 3, + Enqueue = 4, + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/PropertySpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/PropertySpec.cs new file mode 100644 index 0000000000000..13a83fadf7324 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/PropertySpec.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + internal sealed record PropertySpec + { + public PropertySpec(IPropertySymbol property) + { + Name = property.Name; + IsStatic = property.IsStatic; + CanGet = property.GetMethod is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsInitOnly: false }; + CanSet = property.SetMethod is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsInitOnly: false }; + } + + public string Name { get; } + public bool IsStatic { get; } + public bool CanGet { get; } + public bool CanSet { get; } + public required TypeSpec Type { get; init; } + public required string ConfigurationKeyName { get; init; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/Strings.resx new file mode 100644 index 0000000000000..353a2837278dc --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/Strings.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Property '{0}' on type '{1}' is not supported. + + + Did not generate binding logic for a property on a type. + + + Type '{0}' is not supported: {1}. + + + Did not generate binding logic for a type. + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.cs.xlf new file mode 100644 index 0000000000000..a455d29fb3119 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.cs.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.de.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.de.xlf new file mode 100644 index 0000000000000..83f3e0c9d3dc2 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.de.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.es.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.es.xlf new file mode 100644 index 0000000000000..65e1a98fc2cb1 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.es.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.fr.xlf new file mode 100644 index 0000000000000..8b28eb2e7e0dc --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.fr.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.it.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.it.xlf new file mode 100644 index 0000000000000..ea90ac50d329c --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.it.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ja.xlf new file mode 100644 index 0000000000000..7b109d9c36227 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ja.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ko.xlf new file mode 100644 index 0000000000000..769679e7946d8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ko.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pl.xlf new file mode 100644 index 0000000000000..03783e569ef45 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pl.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pt-BR.xlf new file mode 100644 index 0000000000000..606db804a431b --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.pt-BR.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ru.xlf new file mode 100644 index 0000000000000..478a310db9d60 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.ru.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.tr.xlf new file mode 100644 index 0000000000000..c645685a52182 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.tr.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hans.xlf new file mode 100644 index 0000000000000..4125bda3d6932 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hant.xlf new file mode 100644 index 0000000000000..e78a617f37d58 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -0,0 +1,27 @@ + + + + + + Property '{0}' on type '{1}' is not supported. + Property '{0}' on type '{1}' is not supported. + + + + Did not generate binding logic for a property on a type. + Did not generate binding logic for a property on a type. + + + + Type '{0}' is not supported: {1}. + Type '{0}' is not supported: {1}. + + + + Did not generate binding logic for a type. + Did not generate binding logic for a type. + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.cs new file mode 100644 index 0000000000000..2743d9b52f3b8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/SourceGenerationSpec.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. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration +{ + internal sealed record SourceGenerationSpec( + HashSet TypesForBindMethodGen, + HashSet TypesForGetMethodGen, + HashSet TypesForConfigureMethodGen); +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpec.cs new file mode 100644 index 0000000000000..cd500c6b9db06 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpec.cs @@ -0,0 +1,34 @@ +// 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 record TypeSpec + { + public TypeSpec(ITypeSymbol type) + { + DisplayString = type.ToDisplayString(); + SpecialType = type.SpecialType; + IsValueType = type.IsValueType; + } + + public string DisplayString { get; } + + public SpecialType SpecialType { get; } + + public bool IsValueType { get; } + + public bool PassToBindCoreByRef => IsValueType || SpecKind == TypeSpecKind.Array; + + public virtual TypeSpecKind SpecKind { get; init; } + + public virtual ConstructionStrategy ConstructionStrategy { get; init; } + + /// + /// Where in the input compilation we picked up a call to Bind, Get, or Configure. + /// + public required Location? Location { get; init; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpecKind.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpecKind.cs new file mode 100644 index 0000000000000..810a4aaae6eb0 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeSpecKind.cs @@ -0,0 +1,19 @@ +// 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 +{ + internal enum TypeSpecKind + { + StringBasedParse = 0, + Enum = 1, + Object = 2, + Array = 3, + Enumerable = 4, + Dictionary = 5, + IConfigurationSection = 6, + System_Object = 7, + ByteArray = 8, + Nullable = 9, + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Properties/InternalsVisibleTo.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Properties/InternalsVisibleTo.cs index 86944dd2d8695..b4fd88cfbf149 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Properties/InternalsVisibleTo.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Properties/InternalsVisibleTo.cs @@ -4,3 +4,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.Extensions.Configuration.Binder.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs similarity index 64% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs index 141e1dd3d077b..cbeeaae54bb22 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs @@ -2,15 +2,18 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; +using Microsoft.Extensions.Configuration; using Xunit; -namespace Microsoft.Extensions.Configuration.Binder.Test +namespace Microsoft.Extensions +#if BUILDING_SOURCE_GENERATOR_TESTS + .SourceGeneration +#endif + .Configuration.Binder.Tests { - public class ConfigurationCollectionBinding + public partial class ConfigurationBinderCollectionTests { [Fact] public void GetList() @@ -57,7 +60,7 @@ public void GetListNullValues() Assert.Empty(list); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync public void GetListInvalidValues() { var input = new Dictionary @@ -548,7 +551,7 @@ public void ShouldPreserveExistingKeysInDictionaryWithEnumAsKeyType() var config = new ConfigurationBuilder().AddInMemoryCollection(input).Build(); var origin = new Dictionary> { - [KeyEnum.abc] = new Dictionary { [KeyUintEnum.abc] = "val_1" } + [KeyEnum.abc] = new Dictionary { [KeyUintEnum.abc] = "val_1" } }; config.Bind(origin); @@ -571,7 +574,6 @@ public void ShouldPreserveExistingValuesInArrayWhenItIsDictionaryElement() { ["ascii"] = new int[] { 97 } }; - config.Bind(origin); Assert.Equal(new int[] { 97, 98 }, origin["ascii"]); @@ -636,7 +638,7 @@ public void AlreadyInitializedHashSetDictionaryBinding() Assert.Equal("val_3", options.AlreadyInitializedHashSetDictionary["123"].ElementAt(3)); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanOverrideExistingDictionaryKey() { var input = new Dictionary @@ -815,11 +817,14 @@ public void NonStringKeyDictionaryBinding() var options = new OptionsWithDictionary(); config.Bind(options); - +#if BUILDING_SOURCE_GENERATOR_TESTS // Source generator will not touch the property if it is not supported. + Assert.Null(options.NonStringKeyDictionary); +#else Assert.Empty(options.NonStringKeyDictionary); +#endif } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void GetStringArray() { var input = new Dictionary @@ -848,7 +853,7 @@ public void GetStringArray() } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void BindStringArray() { var input = new Dictionary @@ -876,7 +881,7 @@ public void BindStringArray() Assert.Equal("valx", array[3]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void GetAlreadyInitializedArray() { var input = new Dictionary @@ -906,7 +911,7 @@ public void GetAlreadyInitializedArray() Assert.Equal("valx", array[6]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void BindAlreadyInitializedArray() { var input = new Dictionary @@ -937,7 +942,7 @@ public void BindAlreadyInitializedArray() Assert.Equal("valx", array[6]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void ArrayInNestedOptionBinding() { var input = new Dictionary @@ -966,7 +971,7 @@ public void ArrayInNestedOptionBinding() Assert.Equal(12, options.ObjectArray[1].ArrayInNestedOption[2]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void UnsupportedMultidimensionalArrays() { var input = new Dictionary @@ -987,7 +992,7 @@ public void UnsupportedMultidimensionalArrays() exception.Message); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void JaggedArrayBinding() { var input = new Dictionary @@ -1016,7 +1021,7 @@ public void JaggedArrayBinding() Assert.Equal("12", options.JaggedArray[1][2]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void ReadOnlyArrayIsIgnored() { var input = new Dictionary @@ -1035,7 +1040,7 @@ public void ReadOnlyArrayIsIgnored() Assert.Equal(new OptionsWithArrays().ReadOnlyArray, options.ReadOnlyArray); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindUninitializedIEnumerable() { var input = new Dictionary @@ -1063,7 +1068,7 @@ public void CanBindUninitializedIEnumerable() Assert.Equal("valx", array[3]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated() { var input = new Dictionary @@ -1110,7 +1115,7 @@ public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated() Assert.Equal("ExtraItem", options.ICollectionNoSetter.ElementAt(2)); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindInitializedCustomIEnumerableBasedList() { // A field declared as IEnumerable that is instantiated with a class @@ -1140,7 +1145,7 @@ public void CanBindInitializedCustomIEnumerableBasedList() Assert.Equal("val1", array[3]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindInitializedCustomIndirectlyDerivedIEnumerableList() { // A field declared as IEnumerable that is instantiated with a class @@ -1170,7 +1175,7 @@ public void CanBindInitializedCustomIndirectlyDerivedIEnumerableList() Assert.Equal("val1", array[3]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindInitializedIReadOnlyDictionaryAndDoesNotModifyTheOriginal() { // A field declared as IEnumerable that is instantiated with a class @@ -1202,7 +1207,7 @@ public void CanBindInitializedIReadOnlyDictionaryAndDoesNotModifyTheOriginal() Assert.Equal("val_2", InitializedCollectionsOptions.ExistingDictionary["existing_key_2"]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindUninitializedICollection() { var input = new Dictionary @@ -1235,7 +1240,7 @@ public void CanBindUninitializedICollection() Assert.Equal("ExtraItem", options.ICollection.ElementAt(4)); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindUninitializedIList() { var input = new Dictionary @@ -1268,7 +1273,7 @@ public void CanBindUninitializedIList() Assert.Equal("ExtraItem", options.IList[4]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindUninitializedIReadOnlyCollection() { var input = new Dictionary @@ -1296,7 +1301,7 @@ public void CanBindUninitializedIReadOnlyCollection() Assert.Equal("valx", array[3]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindUninitializedIReadOnlyList() { var input = new Dictionary @@ -1324,7 +1329,7 @@ public void CanBindUninitializedIReadOnlyList() Assert.Equal("valx", array[3]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindUninitializedIDictionary() { var input = new Dictionary @@ -1348,7 +1353,7 @@ public void CanBindUninitializedIDictionary() Assert.Equal("val_3", options.IDictionary["ghi"]); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindUninitializedIReadOnlyDictionary() { var input = new Dictionary @@ -1375,7 +1380,7 @@ public void CanBindUninitializedIReadOnlyDictionary() /// /// Replicates scenario from https://github.com/dotnet/runtime/issues/65710 /// - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindWithInterdependentProperties() { var input = new Dictionary @@ -1398,7 +1403,7 @@ public void CanBindWithInterdependentProperties() /// /// Replicates scenario from https://github.com/dotnet/runtime/issues/63479 /// - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void TestCanBindListPropertyWithoutSetter() { var input = new Dictionary @@ -1417,275 +1422,739 @@ public void TestCanBindListPropertyWithoutSetter() Assert.Equal(new[] { "a", "b" }, options.ListPropertyWithoutSetter); } - private class UninitializedCollectionsOptions + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindNonInstantiatedIEnumerableWithItems() { - public IEnumerable IEnumerable { get; set; } - public IDictionary IDictionary { get; set; } - public ICollection ICollection { get; set; } - public IList IList { get; set; } - public IReadOnlyCollection IReadOnlyCollection { get; set; } - public IReadOnlyList IReadOnlyList { get; set; } - public IReadOnlyDictionary IReadOnlyDictionary { get; set; } + var dic = new Dictionary + { + {"NonInstantiatedIEnumerable:0", "Yo1"}, + {"NonInstantiatedIEnumerable:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedIEnumerable.Count()); + Assert.Equal("Yo1", options.NonInstantiatedIEnumerable.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedIEnumerable.ElementAt(1)); } - private class InitializedCollectionsOptions + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindNonInstantiatedISet() { - public InitializedCollectionsOptions() + var dic = new Dictionary { - AlreadyInitializedIEnumerableInterface = ListUsedInIEnumerableFieldAndShouldNotBeTouched; - AlreadyInitializedDictionary = ExistingDictionary; - } + {"NonInstantiatedISet:0", "Yo1"}, + {"NonInstantiatedISet:1", "Yo2"}, + {"NonInstantiatedISet:2", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public List ListUsedInIEnumerableFieldAndShouldNotBeTouched = new() + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedISet.Count); + Assert.Equal("Yo1", options.NonInstantiatedISet.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedISet.ElementAt(1)); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindISetNoSetter() + { + var dic = new Dictionary { - "This was here too", - "Don't touch me!" + {"ISetNoSetter:0", "Yo1"}, + {"ISetNoSetter:1", "Yo2"}, + {"ISetNoSetter:2", "Yo2"}, }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); - public static ReadOnlyDictionary ExistingDictionary = new( - new Dictionary - { - {"existing_key_1", "val_1"}, - {"existing_key_2", "val_2"} - }); + var options = config.Get()!; - public IEnumerable AlreadyInitializedIEnumerableInterface { get; set; } + Assert.Equal(2, options.ISetNoSetter.Count); + Assert.Equal("Yo1", options.ISetNoSetter.ElementAt(0)); + Assert.Equal("Yo2", options.ISetNoSetter.ElementAt(1)); + } - public IEnumerable AlreadyInitializedCustomListDerivedFromIEnumerable { get; set; } = - new CustomListDerivedFromIEnumerable(); +#if NETCOREAPP + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedIReadOnlySet() + { + var dic = new Dictionary + { + {"InstantiatedIReadOnlySet:0", "Yo1"}, + {"InstantiatedIReadOnlySet:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public IEnumerable AlreadyInitializedCustomListIndirectlyDerivedFromIEnumerable { get; set; } = - new CustomListIndirectlyDerivedFromIEnumerable(); + var config = configurationBuilder.Build(); - public IReadOnlyDictionary AlreadyInitializedDictionary { get; set; } + var options = config.Get()!; - public ICollection ICollectionNoSetter { get; } = new List(); + Assert.Equal(2, options.InstantiatedIReadOnlySet.Count); + Assert.Equal("Yo1", options.InstantiatedIReadOnlySet.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedIReadOnlySet.ElementAt(1)); } - private class CustomList : List + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedIReadOnlyWithSomeValues() { - // Add an overload, just to make sure binding picks the right Add method - public void Add(string a, string b) + var dic = new Dictionary { - } + {"InstantiatedIReadOnlySetWithSomeValues:0", "Yo1"}, + {"InstantiatedIReadOnlySetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.InstantiatedIReadOnlySetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(1)); + Assert.Equal("Yo1", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(2)); + Assert.Equal("Yo2", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(3)); } - private class CustomListDerivedFromIEnumerable : IEnumerable + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindNonInstantiatedIReadOnlySet() { - private readonly List _items = new List { "Item1", "Item2" }; + var dic = new Dictionary + { + {"NonInstantiatedIReadOnlySet:0", "Yo1"}, + {"NonInstantiatedIReadOnlySet:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); - public IEnumerator GetEnumerator() => _items.GetEnumerator(); + var options = config.Get()!; - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + Assert.Equal(2, options.NonInstantiatedIReadOnlySet.Count); + Assert.Equal("Yo1", options.NonInstantiatedIReadOnlySet.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedIReadOnlySet.ElementAt(1)); } - internal interface IDerivedOne : IDerivedTwo + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedDictionaryOfIReadOnlySetWithSomeExistingValues() { + var dic = new Dictionary + { + {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:foo:0", "foo-1"}, + {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:foo:1", "foo-2"}, + {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:bar:0", "bar-1"}, + {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:bar:1", "bar-2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(3, options.InstantiatedDictionaryWithReadOnlySetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["item1"].ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["item1"].ElementAt(1)); + + Assert.Equal("foo-1", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["foo"].ElementAt(0)); + Assert.Equal("foo-2", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["foo"].ElementAt(1)); + Assert.Equal("bar-1", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["bar"].ElementAt(0)); + Assert.Equal("bar-2", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["bar"].ElementAt(1)); } +#endif - internal interface IDerivedTwo : IEnumerable + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedReadOnlyDictionary2() { + var dic = new Dictionary + { + {"Items:item3", "3"}, + {"Items:item4", "4"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.Items.Count); + Assert.Equal(1, options.Items["existing-item1"]); + Assert.Equal(2, options.Items["existing-item2"]); + Assert.Equal(3, options.Items["item3"]); + Assert.Equal(4, options.Items["item4"]); + + } - private class CustomListIndirectlyDerivedFromIEnumerable : IDerivedOne + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() { - private readonly List _items = new List { "Item1", "Item2" }; + var dic = new Dictionary + { + {"Dictionary:existing-item1", "666"}, + {"Dictionary:item3", "3"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); - public IEnumerator GetEnumerator() => _items.GetEnumerator(); + var options = config.Get()!; - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + Assert.Equal(3, options.Dictionary.Count); + + // does not overwrite original + Assert.Equal(1, ConfigWithInstantiatedIReadOnlyDictionary._existingDictionary["existing-item1"]); + + Assert.Equal(666, options.Dictionary["existing-item1"]); + Assert.Equal(2, options.Dictionary["existing-item2"]); + Assert.Equal(3, options.Dictionary["item3"]); } - private class CustomDictionary : Dictionary + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void BindNonInstantiatedIReadOnlyDictionary() { + var dic = new Dictionary + { + {"Dictionary:item1", "1"}, + {"Dictionary:item2", "2"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.Dictionary.Count); + + Assert.Equal(1, options.Dictionary["item1"]); + Assert.Equal(2, options.Dictionary["item2"]); } - private class NestedOptions + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void BindInstantiatedConcreteDictionary_OverwritesOriginal() { - public int Integer { get; set; } + var dic = new Dictionary + { + {"Dictionary:existing-item1", "666"}, + {"Dictionary:item3", "3"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public List ListInNestedOption { get; set; } + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(3, options.Dictionary.Count); - public int[] ArrayInNestedOption { get; set; } + // overwrites original + Assert.Equal(666, ConfigWithInstantiatedConcreteDictionary._existingDictionary["existing-item1"]); + Assert.Equal(666, options.Dictionary["existing-item1"]); + Assert.Equal(2, options.Dictionary["existing-item2"]); + Assert.Equal(3, options.Dictionary["item3"]); } - private enum KeyEnum + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedReadOnlyDictionary() { - abc, - def, - ghi + var dic = new Dictionary + { + {"InstantiatedReadOnlyDictionaryWithWithSomeValues:item3", "3"}, + {"InstantiatedReadOnlyDictionaryWithWithSomeValues:item4", "4"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + var resultingDictionary = options.InstantiatedReadOnlyDictionaryWithWithSomeValues; + Assert.Equal(4, resultingDictionary.Count); + Assert.Equal(1, resultingDictionary["existing-item1"]); + Assert.Equal(2, resultingDictionary["existing-item2"]); + Assert.Equal(3, resultingDictionary["item3"]); + Assert.Equal(4, resultingDictionary["item4"]); } - private enum KeyUintEnum : uint + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindNonInstantiatedReadOnlyDictionary() { - abc, - def, - ghi + var dic = new Dictionary + { + {"NonInstantiatedReadOnlyDictionary:item3", "3"}, + {"NonInstantiatedReadOnlyDictionary:item4", "4"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedReadOnlyDictionary.Count); + Assert.Equal(3, options.NonInstantiatedReadOnlyDictionary["item3"]); + Assert.Equal(4, options.NonInstantiatedReadOnlyDictionary["item4"]); } - private class OptionsWithArrays + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindNonInstantiatedDictionaryOfISet() { - public const string InitialValue = "This was here before"; + var dic = new Dictionary + { + {"NonInstantiatedDictionaryWithISet:foo:0", "foo-1"}, + {"NonInstantiatedDictionaryWithISet:foo:1", "foo-2"}, + {"NonInstantiatedDictionaryWithISet:bar:0", "bar-1"}, + {"NonInstantiatedDictionaryWithISet:bar:1", "bar-2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); - public OptionsWithArrays() + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedDictionaryWithISet.Count); + Assert.Equal("foo-1", options.NonInstantiatedDictionaryWithISet["foo"].ElementAt(0)); + Assert.Equal("foo-2", options.NonInstantiatedDictionaryWithISet["foo"].ElementAt(1)); + Assert.Equal("bar-1", options.NonInstantiatedDictionaryWithISet["bar"].ElementAt(0)); + Assert.Equal("bar-2", options.NonInstantiatedDictionaryWithISet["bar"].ElementAt(1)); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedDictionaryOfISet() + { + var dic = new Dictionary { - AlreadyInitializedArray = new string[] { InitialValue, null, null }; - } + {"InstantiatedDictionaryWithHashSet:foo:0", "foo-1"}, + {"InstantiatedDictionaryWithHashSet:foo:1", "foo-2"}, + {"InstantiatedDictionaryWithHashSet:bar:0", "bar-1"}, + {"InstantiatedDictionaryWithHashSet:bar:1", "bar-2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public string[] AlreadyInitializedArray { get; set; } + var config = configurationBuilder.Build(); - public string[] StringArray { get; set; } + var options = config.Get()!; + + Assert.Equal(2, options.InstantiatedDictionaryWithHashSet.Count); + Assert.Equal("foo-1", options.InstantiatedDictionaryWithHashSet["foo"].ElementAt(0)); + Assert.Equal("foo-2", options.InstantiatedDictionaryWithHashSet["foo"].ElementAt(1)); + Assert.Equal("bar-1", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(0)); + Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(1)); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues() + { + var dic = new Dictionary + { + {"InstantiatedDictionaryWithHashSetWithSomeValues:foo:0", "foo-1"}, + {"InstantiatedDictionaryWithHashSetWithSomeValues:foo:1", "foo-2"}, + {"InstantiatedDictionaryWithHashSetWithSomeValues:bar:0", "bar-1"}, + {"InstantiatedDictionaryWithHashSetWithSomeValues:bar:1", "bar-2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - // this should throw because we do not support multidimensional arrays - public string[,] DimensionalArray { get; set; } + var config = configurationBuilder.Build(); - public string[][] JaggedArray { get; set; } + var options = config.Get()!; - public NestedOptions[] ObjectArray { get; set; } + Assert.Equal(3, options.InstantiatedDictionaryWithHashSetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedDictionaryWithHashSetWithSomeValues["item1"].ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedDictionaryWithHashSetWithSomeValues["item1"].ElementAt(1)); - public int[] ReadOnlyArray { get; } = new[] { 1, 2 }; + Assert.Equal("foo-1", options.InstantiatedDictionaryWithHashSetWithSomeValues["foo"].ElementAt(0)); + Assert.Equal("foo-2", options.InstantiatedDictionaryWithHashSetWithSomeValues["foo"].ElementAt(1)); + Assert.Equal("bar-1", options.InstantiatedDictionaryWithHashSetWithSomeValues["bar"].ElementAt(0)); + Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSetWithSomeValues["bar"].ElementAt(1)); } - private class OptionsWithLists + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void ThrowsForCustomIEnumerableCollection() { - public OptionsWithLists() + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(new Dictionary { - AlreadyInitializedList = new List - { - "This was here before" - }; - AlreadyInitializedListInterface = new List - { - "This was here too" - }; - } + ["CustomIEnumerableCollection:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); - public CustomList CustomList { get; set; } + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomCollectionDerivedFromIEnumerableT)), + exception.Message); + } - public List StringList { get; set; } + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void ThrowsForCustomICollection() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + ["CustomCollection:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomCollectionDerivedFromICollectionT)), + exception.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void ThrowsForCustomDictionary() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + ["CustomDictionary:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); - public List IntList { get; set; } + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomDictionary)), + exception.Message); + } - // This cannot be initialized because we cannot - // activate an interface - public IList StringListInterface { get; set; } + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void ThrowsForCustomSet() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + ["CustomSet:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); - public List> NestedLists { get; set; } + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomSet)), + exception.Message); + } - public List AlreadyInitializedList { get; set; } + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedISet() + { + var dic = new Dictionary + { + {"InstantiatedISet:0", "Yo1"}, + {"InstantiatedISet:1", "Yo2"}, + {"InstantiatedISet:2", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public List ObjectList { get; set; } + var config = configurationBuilder.Build(); - public IList AlreadyInitializedListInterface { get; set; } + var options = config.Get()!; - public List ListPropertyWithoutSetter { get; } = new(); + Assert.Equal(2, options.InstantiatedISet.Count()); + Assert.Equal("Yo1", options.InstantiatedISet.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedISet.ElementAt(1)); } - private class OptionsWithDictionary + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedISetWithSomeValues() { - public OptionsWithDictionary() + var dic = new Dictionary { - AlreadyInitializedStringDictionaryInterface = new Dictionary - { - ["123"] = "This was already here" - }; + {"InstantiatedISetWithSomeValues:0", "Yo1"}, + {"InstantiatedISetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - AlreadyInitializedHashSetDictionary = new Dictionary> - { - ["123"] = new HashSet(new[] {"This was already here"}) - }; - } + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.InstantiatedISetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedISetWithSomeValues.ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedISetWithSomeValues.ElementAt(1)); + Assert.Equal("Yo1", options.InstantiatedISetWithSomeValues.ElementAt(2)); + Assert.Equal("Yo2", options.InstantiatedISetWithSomeValues.ElementAt(3)); + } - public Dictionary IntDictionary { get; set; } + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedHashSetWithSomeValues() + { + var dic = new Dictionary + { + {"InstantiatedHashSetWithSomeValues:0", "Yo1"}, + {"InstantiatedHashSetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public Dictionary StringDictionary { get; set; } + var config = configurationBuilder.Build(); - public IDictionary IDictionaryNoSetter { get; } = new Dictionary(); + var options = config.Get()!; - public Dictionary ObjectDictionary { get; set; } + Assert.Equal(4, options.InstantiatedHashSetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedHashSetWithSomeValues.ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedHashSetWithSomeValues.ElementAt(1)); + Assert.Equal("Yo1", options.InstantiatedHashSetWithSomeValues.ElementAt(2)); + Assert.Equal("Yo2", options.InstantiatedHashSetWithSomeValues.ElementAt(3)); + } - public Dictionary> ISetDictionary { get; set; } - public Dictionary> ListDictionary { get; set; } + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindNonInstantiatedHashSet() + { + var dic = new Dictionary + { + {"NonInstantiatedHashSet:0", "Yo1"}, + {"NonInstantiatedHashSet:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public Dictionary NonStringKeyDictionary { get; set; } + var config = configurationBuilder.Build(); - // This cannot be initialized because we cannot - // activate an interface - public IDictionary StringDictionaryInterface { get; set; } + var options = config.Get()!; - public IDictionary AlreadyInitializedStringDictionaryInterface { get; set; } - public IDictionary> AlreadyInitializedHashSetDictionary { get; set; } + Assert.Equal(2, options.NonInstantiatedHashSet.Count); + Assert.Equal("Yo1", options.NonInstantiatedHashSet.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedHashSet.ElementAt(1)); } - private class OptionsWithInterdependentProperties + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedSortedSetWithSomeValues() { - public IEnumerable FilteredConfigValues => ConfigValues.Where(p => p > 10); - public IEnumerable ConfigValues { get; set; } + var dic = new Dictionary + { + {"InstantiatedSortedSetWithSomeValues:0", "Yo1"}, + {"InstantiatedSortedSetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.InstantiatedSortedSetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedSortedSetWithSomeValues.ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedSortedSetWithSomeValues.ElementAt(1)); + Assert.Equal("Yo1", options.InstantiatedSortedSetWithSomeValues.ElementAt(2)); + Assert.Equal("Yo2", options.InstantiatedSortedSetWithSomeValues.ElementAt(3)); } - [Fact] - public void DifferentDictionaryBindingCasesTest() + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindNonInstantiatedSortedSetWithSomeValues() { - var dic = new Dictionary() { { "key", "value" } }; - var config = new ConfigurationBuilder() - .AddInMemoryCollection(dic) - .Build(); + var dic = new Dictionary + { + {"NonInstantiatedSortedSetWithSomeValues:0", "Yo1"}, + {"NonInstantiatedSortedSetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - Assert.Single(config.Get>()); - Assert.Single(config.Get>()); - Assert.Single(config.Get>()); - Assert.Single(config.Get>()); + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedSortedSetWithSomeValues.Count); + Assert.Equal("Yo1", options.NonInstantiatedSortedSetWithSomeValues.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedSortedSetWithSomeValues.ElementAt(1)); } - public class ImplementerOfIDictionaryClass : IDictionary + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void DoesNotBindInstantiatedISetWithUnsupportedKeys() { - private Dictionary _dict = new(); + var dic = new Dictionary + { + {"HashSetWithUnsupportedKey:0", "Yo1"}, + {"HashSetWithUnsupportedKey:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); - public TValue this[TKey key] { get => _dict[key]; set => _dict[key] = value; } + var options = config.Get()!; + + Assert.Equal(0, options.HashSetWithUnsupportedKey.Count); + } - public ICollection Keys => _dict.Keys; + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void DoesNotBindUninstantiatedISetWithUnsupportedKeys() + { + var dic = new Dictionary + { + {"UninstantiatedHashSetWithUnsupportedKey:0", "Yo1"}, + {"UninstantiatedHashSetWithUnsupportedKey:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public ICollection Values => _dict.Values; + var config = configurationBuilder.Build(); - public int Count => _dict.Count; + var options = config.Get()!; - public bool IsReadOnly => false; + Assert.Null(options.UninstantiatedHashSetWithUnsupportedKey); + } - public void Add(TKey key, TValue value) => _dict.Add(key, value); + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedIEnumerableWithItems() + { + var dic = new Dictionary + { + {"InstantiatedIEnumerable:0", "Yo1"}, + {"InstantiatedIEnumerable:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public void Add(KeyValuePair item) => _dict.Add(item.Key, item.Value); + var config = configurationBuilder.Build(); - public void Clear() => _dict.Clear(); + var options = config.Get()!; - public bool Contains(KeyValuePair item) => _dict.Contains(item); + Assert.Equal(2, options.InstantiatedIEnumerable.Count()); + Assert.Equal("Yo1", options.InstantiatedIEnumerable.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1)); + } - public bool ContainsKey(TKey key) => _dict.ContainsKey(key); + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedCustomICollectionWithoutAnAddMethodWithItems() + { + var dic = new Dictionary + { + {"InstantiatedCustomICollectionWithoutAnAddMethod:0", "Yo1"}, + {"InstantiatedCustomICollectionWithoutAnAddMethod:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public void CopyTo(KeyValuePair[] array, int arrayIndex) => throw new NotImplementedException(); + var config = configurationBuilder.Build(); - public IEnumerator> GetEnumerator() => _dict.GetEnumerator(); + var options = config.Get()!; - public bool Remove(TKey key) => _dict.Remove(key); + Assert.Equal(2, options.InstantiatedCustomICollectionWithoutAnAddMethod.Count); + Assert.Equal("Yo1", options.InstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1)); + } - public bool Remove(KeyValuePair item) => _dict.Remove(item.Key); + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindNonInstantiatedCustomICollectionWithoutAnAddMethodWithItems() + { + var dic = new Dictionary + { + {"NonInstantiatedCustomICollectionWithoutAnAddMethod:0", "Yo1"}, + {"NonInstantiatedCustomICollectionWithoutAnAddMethod:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); - public bool TryGetValue(TKey key, out TValue value) => _dict.TryGetValue(key, out value); + var config = configurationBuilder.Build(); - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => _dict.GetEnumerator(); + var options = config.Get()!; - // The following are members which have the same names as the IDictionary<,> members. - // The following members test that there's no System.Reflection.AmbiguousMatchException when binding to the dictionary. - private string? v; - public string? this[string key] { get => v; set => v = value; } - public bool TryGetValue() { return true; } + Assert.Equal(2, options.NonInstantiatedCustomICollectionWithoutAnAddMethod.Count); + Assert.Equal("Yo1", options.NonInstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1)); } - public class ExtendedDictionary : Dictionary + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedICollectionWithItems() { + var dic = new Dictionary + { + {"InstantiatedICollection:0", "Yo1"}, + {"InstantiatedICollection:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.InstantiatedICollection.Count()); + Assert.Equal("Yo1", options.InstantiatedICollection.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedICollection.ElementAt(1)); } - private class OptionsWithDifferentCollectionInterfaces + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedIReadOnlyCollectionWithItems() + { + var dic = new Dictionary + { + {"InstantiatedIReadOnlyCollection:0", "Yo1"}, + {"InstantiatedIReadOnlyCollection:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.InstantiatedIReadOnlyCollection.Count); + Assert.Equal("Yo1", options.InstantiatedIReadOnlyCollection.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedIReadOnlyCollection.ElementAt(1)); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void CanBindInstantiatedIEnumerableWithNullItems() + { + var dic = new Dictionary + { + {"InstantiatedIEnumerable:0", null}, + {"InstantiatedIEnumerable:1", "Yo1"}, + {"InstantiatedIEnumerable:2", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.InstantiatedIEnumerable.Count()); + Assert.Equal("Yo1", options.InstantiatedIEnumerable.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1)); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void DifferentDictionaryBindingCasesTest() + { + var dic = new Dictionary() { { "key", "value" } }; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(dic) + .Build(); + + Assert.Single(config.Get>()); + Assert.Single(config.Get>()); + Assert.Single(config.Get>()); + Assert.Single(config.Get>()); + } + + public class OptionsWithDifferentCollectionInterfaces { private static IEnumerable s_instantiatedIEnumerable = new List { "value1", "value2" }; public bool IsSameInstantiatedIEnumerable() => object.ReferenceEquals(s_instantiatedIEnumerable, InstantiatedIEnumerable); @@ -1735,7 +2204,7 @@ private class OptionsWithDifferentCollectionInterfaces public IReadOnlyList UnInstantiatedIReadOnlyList { get; set; } } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void TestOptionsWithDifferentCollectionInterfaces() { var input = new Dictionary @@ -1843,7 +2312,7 @@ public void TestOptionsWithDifferentCollectionInterfaces() Assert.Equal(new string[] { "r", "e" }, options.UnInstantiatedIReadOnlyCollection); } - [Fact] + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void TestMutatingDictionaryValues() { IConfiguration config = new ConfigurationBuilder() @@ -1862,5 +2331,9 @@ public void TestMutatingDictionaryValues() Assert.Equal("InitialValue", dict["Key"][0]); Assert.Equal("NewValue", dict["Key"][1]); } + + // Test behavior for root level arrays. + + // Tests for TypeConverter usage. } } 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 new file mode 100644 index 0000000000000..9a90d0e0481b4 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Helpers.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions +#if BUILDING_SOURCE_GENERATOR_TESTS + .SourceGeneration +#endif + .Configuration.Binder.Tests +{ + public static class TestHelpers + { + public static bool NotSourceGenMode +#if BUILDING_SOURCE_GENERATOR_TESTS + = false; +#else + = true; +#endif + } + + #region // Shared test classes + public class ComplexOptions + { + private static Dictionary _existingDictionary = new() + { + {"existing-item1", 1}, + {"existing-item2", 2}, + }; + + public ComplexOptions() + { + Nested = new NestedOptions(); + Virtual = "complex"; + } + + public NestedOptions Nested { get; set; } + public int Integer { get; set; } + public bool Boolean { get; set; } + public virtual string Virtual { get; set; } + public object Object { get; set; } + + public string PrivateSetter { get; private set; } + public string ProtectedSetter { get; protected set; } + public string InternalSetter { get; internal set; } + public static string StaticProperty { get; set; } + + private string PrivateProperty { get; set; } + internal string InternalProperty { get; set; } + protected string ProtectedProperty { get; set; } + + [ConfigurationKeyName("Named_Property")] + public string NamedProperty { get; set; } + + protected string ProtectedPrivateSet { get; private set; } + + private string PrivateReadOnly { get; } + internal string InternalReadOnly { get; } + protected string ProtectedReadOnly { get; } + + public string ReadOnly + { + get { return null; } + } + + public ISet NonInstantiatedISet { get; set; } = null!; + public HashSet NonInstantiatedHashSet { get; set; } = null!; + public IDictionary> NonInstantiatedDictionaryWithISet { get; set; } = null!; + public IDictionary> InstantiatedDictionaryWithHashSet { get; set; } = + new Dictionary>(); + + public IDictionary> InstantiatedDictionaryWithHashSetWithSomeValues { get; set; } = + new Dictionary> + { + {"item1", new HashSet(new[] {"existing1", "existing2"})} + }; + + public IEnumerable NonInstantiatedIEnumerable { get; set; } = null!; + + public ISet InstantiatedISet { get; set; } = new HashSet(); + + public ISet ISetNoSetter { get; } = new HashSet(); + + public HashSet InstantiatedHashSetWithSomeValues { get; set; } = + new HashSet(new[] { "existing1", "existing2" }); + + public SortedSet InstantiatedSortedSetWithSomeValues { get; set; } = + new SortedSet(new[] { "existing1", "existing2" }); + + public SortedSet NonInstantiatedSortedSetWithSomeValues { get; set; } = null!; + + public ISet InstantiatedISetWithSomeValues { get; set; } = + new HashSet(new[] { "existing1", "existing2" }); + + public ISet HashSetWithUnsupportedKey { get; set; } = + new HashSet(); + + public ISet UninstantiatedHashSetWithUnsupportedKey { get; set; } + +#if NETCOREAPP + public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = new HashSet(); + public IReadOnlySet InstantiatedIReadOnlySetWithSomeValues { get; set; } = + new HashSet(new[] { "existing1", "existing2" }); + public IReadOnlySet NonInstantiatedIReadOnlySet { get; set; } + public IDictionary> InstantiatedDictionaryWithReadOnlySetWithSomeValues { get; set; } = + new Dictionary> + { + {"item1", new HashSet(new[] {"existing1", "existing2"})} + }; +#endif + public IReadOnlyDictionary InstantiatedReadOnlyDictionaryWithWithSomeValues { get; set; } = + _existingDictionary; + + public IReadOnlyDictionary NonInstantiatedReadOnlyDictionary { get; set; } + + public CustomICollectionWithoutAnAddMethod InstantiatedCustomICollectionWithoutAnAddMethod { get; set; } = new(); + public CustomICollectionWithoutAnAddMethod NonInstantiatedCustomICollectionWithoutAnAddMethod { get; set; } + + public IEnumerable InstantiatedIEnumerable { get; set; } = new List(); + public ICollection InstantiatedICollection { get; set; } = new List(); + public IReadOnlyCollection InstantiatedIReadOnlyCollection { get; set; } = new List(); + } + + public class NestedOptions + { + public int Integer { get; set; } + } + + public class UnsupportedTypeInHashSet { } + + public class CustomICollectionWithoutAnAddMethod : ICollection + { + private readonly List _items = new(); + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void ICollection.Add(string item) => _items.Add(item); + + public void Clear() => _items.Clear(); + + public bool Contains(string item) => _items.Contains(item); + + public void CopyTo(string[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex); + + public bool Remove(string item) => _items.Remove(item); + + public int Count => _items.Count; + public bool IsReadOnly => false; + } + #endregion +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs new file mode 100644 index 0000000000000..9242ae915ab3f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs @@ -0,0 +1,335 @@ +// 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; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Microsoft.Extensions +#if BUILDING_SOURCE_GENERATOR_TESTS + .SourceGeneration +#endif + .Configuration.Binder.Tests +{ + public partial class ConfigurationBinderCollectionTests + { + public class UninitializedCollectionsOptions + { + public IEnumerable IEnumerable { get; set; } + public IDictionary IDictionary { get; set; } + public ICollection ICollection { get; set; } + public IList IList { get; set; } + public IReadOnlyCollection IReadOnlyCollection { get; set; } + public IReadOnlyList IReadOnlyList { get; set; } + public IReadOnlyDictionary IReadOnlyDictionary { get; set; } + } + + public class InitializedCollectionsOptions + { + public InitializedCollectionsOptions() + { + AlreadyInitializedIEnumerableInterface = ListUsedInIEnumerableFieldAndShouldNotBeTouched; + AlreadyInitializedDictionary = ExistingDictionary; + } + + public List ListUsedInIEnumerableFieldAndShouldNotBeTouched = new() + { + "This was here too", + "Don't touch me!" + }; + + public static ReadOnlyDictionary ExistingDictionary = new( + new Dictionary + { + {"existing_key_1", "val_1"}, + {"existing_key_2", "val_2"} + }); + + public IEnumerable AlreadyInitializedIEnumerableInterface { get; set; } + + public IEnumerable AlreadyInitializedCustomListDerivedFromIEnumerable { get; set; } = + new CustomListDerivedFromIEnumerable(); + + public IEnumerable AlreadyInitializedCustomListIndirectlyDerivedFromIEnumerable { get; set; } = + new CustomListIndirectlyDerivedFromIEnumerable(); + + public IReadOnlyDictionary AlreadyInitializedDictionary { get; set; } + + public ICollection ICollectionNoSetter { get; } = new List(); + } + + public class CustomList : List + { + // Add an overload, just to make sure binding picks the right Add method + public void Add(string a, string b) + { + } + } + + public class CustomListDerivedFromIEnumerable : IEnumerable + { + private readonly List _items = new List { "Item1", "Item2" }; + + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + internal interface IDerivedOne : IDerivedTwo + { + } + + internal interface IDerivedTwo : IEnumerable + { + } + + public class CustomListIndirectlyDerivedFromIEnumerable : IDerivedOne + { + private readonly List _items = new List { "Item1", "Item2" }; + + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public class CustomDictionary : Dictionary + { + } + + public class NestedOptions + { + public int Integer { get; set; } + + public List ListInNestedOption { get; set; } + + public int[] ArrayInNestedOption { get; set; } + } + + public enum KeyEnum + { + abc, + def, + ghi + } + + public enum KeyUintEnum : uint + { + abc, + def, + ghi + } + + public class OptionsWithArrays + { + public const string InitialValue = "This was here before"; + + public OptionsWithArrays() + { + AlreadyInitializedArray = new string[] { InitialValue, null, null }; + } + + public string[] AlreadyInitializedArray { get; set; } + + public string[] StringArray { get; set; } + + // this should throw because we do not support multidimensional arrays + public string[,] DimensionalArray { get; set; } + + public string[][] JaggedArray { get; set; } + + public NestedOptions[] ObjectArray { get; set; } + + public int[] ReadOnlyArray { get; } = new[] { 1, 2 }; + } + + public class OptionsWithLists + { + public OptionsWithLists() + { + AlreadyInitializedList = new List + { + "This was here before" + }; + AlreadyInitializedListInterface = new List + { + "This was here too" + }; + } + + public CustomList CustomList { get; set; } + + public List StringList { get; set; } + + public List IntList { get; set; } + + // This cannot be initialized because we cannot + // activate an interface + public IList StringListInterface { get; set; } + + public List> NestedLists { get; set; } + + public List AlreadyInitializedList { get; set; } + + public List ObjectList { get; set; } + + public IList AlreadyInitializedListInterface { get; set; } + + public List ListPropertyWithoutSetter { get; } = new(); + } + + public class OptionsWithDictionary + { + public OptionsWithDictionary() + { + AlreadyInitializedStringDictionaryInterface = new Dictionary + { + ["123"] = "This was already here" + }; + + AlreadyInitializedHashSetDictionary = new Dictionary> + { + ["123"] = new HashSet(new[] { "This was already here" }) + }; + } + + public Dictionary IntDictionary { get; set; } + + public Dictionary StringDictionary { get; set; } + + public IDictionary IDictionaryNoSetter { get; } = new Dictionary(); + + public Dictionary ObjectDictionary { get; set; } + + public Dictionary> ISetDictionary { get; set; } + public Dictionary> ListDictionary { get; set; } + + public Dictionary NonStringKeyDictionary { get; set; } + + // This cannot be initialized because we cannot + // activate an interface + public IDictionary StringDictionaryInterface { get; set; } + + public IDictionary AlreadyInitializedStringDictionaryInterface { get; set; } + public IDictionary> AlreadyInitializedHashSetDictionary { get; set; } + } + + public class OptionsWithInterdependentProperties + { + public IEnumerable FilteredConfigValues => ConfigValues.Where(p => p > 10); + public IEnumerable ConfigValues { get; set; } + } + + public class ImplementerOfIDictionaryClass : IDictionary + { + private Dictionary _dict = new(); + + public TValue this[TKey key] { get => _dict[key]; set => _dict[key] = value; } + + public ICollection Keys => _dict.Keys; + + public ICollection Values => _dict.Values; + + public int Count => _dict.Count; + + public bool IsReadOnly => false; + + public void Add(TKey key, TValue value) => _dict.Add(key, value); + + public void Add(KeyValuePair item) => _dict.Add(item.Key, item.Value); + + public void Clear() => _dict.Clear(); + + public bool Contains(KeyValuePair item) => _dict.Contains(item); + + public bool ContainsKey(TKey key) => _dict.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => throw new NotImplementedException(); + + public IEnumerator> GetEnumerator() => _dict.GetEnumerator(); + + public bool Remove(TKey key) => _dict.Remove(key); + + public bool Remove(KeyValuePair item) => _dict.Remove(item.Key); + + public bool TryGetValue(TKey key, out TValue value) => _dict.TryGetValue(key, out value); + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => _dict.GetEnumerator(); + + // The following are members which have the same names as the IDictionary<,> members. + // The following members test that there's no System.Reflection.AmbiguousMatchException when binding to the dictionary. + private string? v; + public string? this[string key] { get => v; set => v = value; } + public bool TryGetValue() { return true; } + + } + + public class ExtendedDictionary : Dictionary + { + } + + public class Foo + { + public IReadOnlyDictionary Items { get; set; } = + new Dictionary { { "existing-item1", 1 }, { "existing-item2", 2 } }; + + } + + public class MyClassWithCustomSet + { + public ICustomSet CustomSet { get; set; } + } + + public class MyClassWithCustomDictionary + { + public ICustomDictionary CustomDictionary { get; set; } + } + + public class ConfigWithInstantiatedIReadOnlyDictionary + { + public static Dictionary _existingDictionary = new() + { + {"existing-item1", 1}, + {"existing-item2", 2}, + }; + + public IReadOnlyDictionary Dictionary { get; set; } = + _existingDictionary; + } + + public class ConfigWithNonInstantiatedReadOnlyDictionary + { + public IReadOnlyDictionary Dictionary { get; set; } = null!; + } + + public class ConfigWithInstantiatedConcreteDictionary + { + public static Dictionary _existingDictionary = new() + { + {"existing-item1", 1}, + {"existing-item2", 2}, + }; + + public Dictionary Dictionary { get; set; } = + _existingDictionary; + } + + public class MyClassWithCustomCollections + { + public ICustomCollectionDerivedFromIEnumerableT CustomIEnumerableCollection { get; set; } + public ICustomCollectionDerivedFromICollectionT CustomCollection { get; set; } + } + + public interface ICustomCollectionDerivedFromIEnumerableT : IEnumerable { } + public interface ICustomCollectionDerivedFromICollectionT : ICollection { } + + public interface ICustomSet : ISet + { + } + + public interface ICustomDictionary : IDictionary + { + } + } +} 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 new file mode 100644 index 0000000000000..650f4d004c71a --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs @@ -0,0 +1,570 @@ +// 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.Linq; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.Extensions +#if BUILDING_SOURCE_GENERATOR_TESTS + .SourceGeneration +#endif + .Configuration.Binder.Tests +{ + public partial class ConfigurationBinderTests + { + public class DerivedOptions : ComplexOptions + { + public override string Virtual + { + get + { + return base.Virtual; + } + set + { + base.Virtual = "Derived:" + value; + } + } + } + + public class GenericOptions + { + public T Value { get; set; } + } + + public class OptionsWithNesting + { + public NestedOptions Nested { get; set; } + + public class NestedOptions + { + public int Value { get; set; } + } + } + + public class ConfigurationInterfaceOptions + { + public IConfigurationSection Section { get; set; } + } + + public class DerivedOptionsWithIConfigurationSection : DerivedOptions + { + public IConfigurationSection DerivedSection { get; set; } + } + + public record struct RecordStructTypeOptions(string Color, int Length); + + public record RecordOptionsWithNesting(int Number, RecordOptionsWithNesting.RecordNestedOptions Nested1, + RecordOptionsWithNesting.RecordNestedOptions Nested2 = null!) + { + public record RecordNestedOptions(string ValueA, int ValueB); + } + + // Here, the constructor has three parameters, but not all of those match + // match to a property or field + public class ClassWhereParametersDoNotMatchProperties + { + public string Name { get; } + public string Address { get; } + + public ClassWhereParametersDoNotMatchProperties(string name, string address, int age) + { + Name = name; + Address = address; + } + } + + // Here, the constructor has three parameters, and two of them match properties + // and one of them match a field. + public class ClassWhereParametersMatchPropertiesAndFields + { + private int Age; + + public string Name { get; } + public string Address { get; } + + public ClassWhereParametersMatchPropertiesAndFields(string name, string address, int age) + { + Name = name; + Address = address; + Age = age; + } + + public int GetAge() => Age; + } + + public record RecordWhereParametersHaveDefaultValue(string Name, string Address, int Age = 42); + + public record ClassWhereParametersHaveDefaultValue + { + public string? Name { get; } + public string Address { get; } + public int Age { get; } + + public ClassWhereParametersHaveDefaultValue(string? name, string address, int age = 42) + { + Name = name; + Address = address; + Age = age; + } + } + + + public record RecordTypeOptions(string Color, int Length); + + public record Line(string Color, int Length, int Thickness); + + public class ClassWithMatchingParametersAndProperties + { + private readonly string _color; + + public ClassWithMatchingParametersAndProperties(string Color, int Length) + { + _color = Color; + this.Length = Length; + } + + public int Length { get; set; } + + public string Color + { + get => _color; + init => _color = "the color is " + value; + } + } + + public readonly record struct ReadonlyRecordStructTypeOptions(string Color, int Length); + + public class ContainerWithNestedImmutableObject + { + public string ContainerName { get; set; } + public ImmutableLengthAndColorClass LengthAndColor { get; set; } + } + + public struct MutableStructWithConstructor + { + public MutableStructWithConstructor(string randomParameter) + { + Color = randomParameter; + Length = randomParameter.Length; + } + + public string Color { get; set; } + public int Length { get; set; } + } + + public class ImmutableLengthAndColorClass + { + public ImmutableLengthAndColorClass(string color, int length) + { + Color = color; + Length = length; + } + + public string Color { get; } + public int Length { get; } + } + + public class ImmutableClassWithOneParameterizedConstructor + { + public ImmutableClassWithOneParameterizedConstructor(string string1, int int1, string string2, int int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + Int2 = int2; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class ImmutableClassWithOneParameterizedConstructorButWithInParameter + { + public ImmutableClassWithOneParameterizedConstructorButWithInParameter(in string string1, int int1, string string2, int int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + Int2 = int2; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class ImmutableClassWithOneParameterizedConstructorButWithRefParameter + { + public ImmutableClassWithOneParameterizedConstructorButWithRefParameter(string string1, ref int int1, string string2, int int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + Int2 = int2; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class ImmutableClassWithOneParameterizedConstructorButWithOutParameter + { + public ImmutableClassWithOneParameterizedConstructorButWithOutParameter(string string1, int int1, + string string2, out decimal int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + int2 = 0; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class ImmutableClassWithMultipleParameterizedConstructors + { + public ImmutableClassWithMultipleParameterizedConstructors(string string1, int int1) + { + String1 = string1; + Int1 = int1; + } + + public ImmutableClassWithMultipleParameterizedConstructors(string string1, int int1, string string2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + } + + public ImmutableClassWithMultipleParameterizedConstructors(string string1, int int1, string string2, int int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + Int2 = int2; + } + + public ImmutableClassWithMultipleParameterizedConstructors(string string1) + { + String1 = string1; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class SemiImmutableClass + { + public SemiImmutableClass(string color, int length) + { + Color = color; + Length = length; + } + + public string Color { get; } + public int Length { get; } + public decimal Thickness { get; set; } + } + + public class SemiImmutableClassWithInit + { + public SemiImmutableClassWithInit(string color, int length) + { + Color = color; + Length = length; + } + + public string Color { get; } + public int Length { get; } + public decimal Thickness { get; init; } + } + + public struct ValueTypeOptions + { + public int MyInt32 { get; set; } + public string MyString { get; set; } + } + + public class ByteArrayOptions + { + public byte[] MyByteArray { get; set; } + } + + public enum TestSettingsEnum + { + Option1, + Option2, + } + + public class CollectionsBindingWithErrorOnUnknownConfiguration + { + public class MyModelContainingArray + { + public TestSettingsEnum[] Enums { get; set; } + } + + public class MyModelContainingADictionary + { + public Dictionary Enums { get; set; } + } + + [Fact] + public void WithFlagUnset_NoExceptionIsThrownWhenFailingToParseEnumsInAnArrayAndValidItemsArePreserved() + { + var dic = new Dictionary + { + {"Section:Enums:0", "Option1"}, + {"Section:Enums:1", "Option3"}, // invalid - ignored + {"Section:Enums:2", "Option4"}, // invalid - ignored + {"Section:Enums:3", "Option2"}, + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + var configSection = config.GetSection("Section"); + + var model = configSection.Get(o => o.ErrorOnUnknownConfiguration = false); + + Assert.Equal(2, model.Enums.Length); + Assert.Equal(TestSettingsEnum.Option1, model.Enums[0]); + Assert.Equal(TestSettingsEnum.Option2, model.Enums[1]); + } + + [Fact] + public void WithFlagUnset_NoExceptionIsThrownWhenFailingToParseEnumsInADictionaryAndValidItemsArePreserved() + { + var dic = new Dictionary + { + {"Section:Enums:First", "Option1"}, + {"Section:Enums:Second", "Option3"}, // invalid - ignored + {"Section:Enums:Third", "Option4"}, // invalid - ignored + {"Section:Enums:Fourth", "Option2"}, + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + var configSection = config.GetSection("Section"); + + var model = configSection.Get(o => + o.ErrorOnUnknownConfiguration = false); + + Assert.Equal(2, model.Enums.Count); + Assert.Equal(TestSettingsEnum.Option1, model.Enums["First"]); + Assert.Equal(TestSettingsEnum.Option2, model.Enums["Fourth"]); + } + + [Fact] + public void WithFlagSet_AnExceptionIsThrownWhenFailingToParseEnumsInAnArray() + { + var dic = new Dictionary + { + {"Section:Enums:0", "Option1"}, + {"Section:Enums:1", "Option3"}, // invalid - exception thrown + {"Section:Enums:2", "Option1"}, + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + var configSection = config.GetSection("Section"); + + var exception = Assert.Throws( + () => configSection.Get(o => o.ErrorOnUnknownConfiguration = true)); + + Assert.Equal( + SR.Format(SR.Error_GeneralErrorWhenBinding, nameof(BinderOptions.ErrorOnUnknownConfiguration)), + exception.Message); + } + + [Fact] + public void WithFlagSet_AnExceptionIsThrownWhenFailingToParseEnumsInADictionary() + { + var dic = new Dictionary + { + {"Section:Enums:First", "Option1"}, + {"Section:Enums:Second", "Option3"}, // invalid - exception thrown + {"Section:Enums:Third", "Option1"}, + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + var configSection = config.GetSection("Section"); + + var exception = Assert.Throws( + () => configSection.Get(o => + o.ErrorOnUnknownConfiguration = true)); + + Assert.Equal( + SR.Format(SR.Error_GeneralErrorWhenBinding, nameof(BinderOptions.ErrorOnUnknownConfiguration)), + exception.Message); + } + } + + public record RootConfig(NestedConfig Nested); + + public record NestedConfig(string MyProp); + + public class OptionWithCollectionProperties + { + private int _otherCode; + private ICollection blacklist = new HashSet(); + + public ICollection Blacklist + { + get => this.blacklist; + set + { + this.blacklist = value ?? new HashSet(); + this.ParsedBlacklist = this.blacklist.Select(b => b).ToList(); + } + } + + public int HttpStatusCode { get; set; } = 0; + + // ParsedBlacklist initialized using the setter of Blacklist. + public ICollection ParsedBlacklist { get; private set; } = new HashSet(); + + // This property not having any match in the configuration. Still the setter need to be called during the binding. + public int OtherCode + { + get => _otherCode; + set => _otherCode = value == 0 ? 2 : value; + } + } + + public interface ISomeInterface + { + } + + public class ClassWithoutPublicConstructor + { + private ClassWithoutPublicConstructor() + { + } + } + + public class ThrowsWhenActivated + { + public ThrowsWhenActivated() + { + throw new Exception(); + } + } + + public class NestedOptions1 + { + public NestedOptions2 NestedOptions2Property { get; set; } + } + + public class NestedOptions2 + { + public ISomeInterface ISomeInterfaceProperty { get; set; } + } + + public class TestOptions + { + public ISomeInterface ISomeInterfaceProperty { get; set; } + + public ClassWithoutPublicConstructor ClassWithoutPublicConstructorProperty { get; set; } + public ClassWhereParametersDoNotMatchProperties ClassWhereParametersDoNotMatchPropertiesProperty { get; set; } + public Line LineProperty { get; set; } + public ClassWhereParametersHaveDefaultValue ClassWhereParametersHaveDefaultValueProperty { get; set; } + public ClassWhereParametersMatchPropertiesAndFields ClassWhereParametersMatchPropertiesAndFieldsProperty { get; set; } + public RecordWhereParametersHaveDefaultValue RecordWhereParametersHaveDefaultValueProperty { get; set; } + + public int IntProperty { get; set; } + + public ThrowsWhenActivated ThrowsWhenActivatedProperty { get; set; } + + public NestedOptions1 NestedOptionsProperty { get; set; } + } + + public class ClassWithReadOnlyPropertyThatThrows + { + public string StringThrows => throw new InvalidOperationException(nameof(StringThrows)); + + public IEnumerable EnumerableThrows => throw new InvalidOperationException(nameof(EnumerableThrows)); + + public string Safe { get; set; } + } + + public struct StructWithNestedStructs + { + public Nested ReadWriteNestedStruct { get; set; } + + public Nested ReadOnlyNestedStruct { get; } + + public Nested? NullableNestedStruct { get; set; } + + public struct Nested + { + public string String { get; set; } + public DeeplyNested DeeplyNested { get; set; } + } + + public struct DeeplyNested + { + public int Int32 { get; set; } + public bool Boolean { get; set; } + } + } + + public class BaseClassWithVirtualProperty + { + private string? PrivateProperty { get; set; } + + public virtual string[] Test { get; set; } = System.Array.Empty(); + + public virtual string? TestGetSetOverridden { get; set; } + public virtual string? TestGetOverridden { get; set; } + public virtual string? TestSetOverridden { get; set; } + + private string? _testVirtualSet; + public virtual string? TestVirtualSet + { + set => _testVirtualSet = value; + } + + public virtual string? TestNoOverridden { get; set; } + + public string? ExposePrivatePropertyValue() => PrivateProperty; + } + + public class ClassOverridingVirtualProperty : BaseClassWithVirtualProperty + { + public override string[] Test { get => base.Test; set => base.Test = value; } + + public override string? TestGetSetOverridden { get; set; } + public override string? TestGetOverridden => base.TestGetOverridden; + public override string? TestSetOverridden + { + set => base.TestSetOverridden = value; + } + + private string? _testVirtualSet; + public override string? TestVirtualSet + { + set => _testVirtualSet = value; + } + + public string? ExposeTestVirtualSet() => _testVirtualSet; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs new file mode 100644 index 0000000000000..4518348cbf540 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -0,0 +1,1438 @@ +// 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.ComponentModel; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Test; +using Xunit; + +namespace Microsoft.Extensions +#if BUILDING_SOURCE_GENERATOR_TESTS + .SourceGeneration +#endif + .Configuration.Binder.Tests +{ + public partial class ConfigurationBinderTests + { + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for records. + public void BindWithNestedTypesWithReadOnlyProperties() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "Nested:MyProp", "Dummy" } + }) + .Build(); + + var result = configuration.Get(); + + Assert.Equal("Dummy", result.Nested.MyProp); + } + + [Fact] + public void EnumBindCaseInsensitiveNotThrows() + { + var dic = new Dictionary + { + {"Section:Option1", "opt1"}, + {"Section:option2", "opt2"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + var configSection = config.GetSection("Section"); + + var configOptions = new Dictionary(); + configSection.Bind(configOptions); + + Assert.Equal("opt1", configOptions[TestSettingsEnum.Option1]); + Assert.Equal("opt2", configOptions[TestSettingsEnum.Option2]); + } + + [Fact] + public void CanBindIConfigurationSection() + { + var dic = new Dictionary + { + {"Section:Integer", "-2"}, + {"Section:Boolean", "TRUe"}, + {"Section:Nested:Integer", "11"}, + {"Section:Virtual", "Sup"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + var childOptions = options.Section.Get(); + + Assert.True(childOptions.Boolean); + Assert.Equal(-2, childOptions.Integer); + Assert.Equal(11, childOptions.Nested.Integer); + Assert.Equal("Derived:Sup", childOptions.Virtual); + + Assert.Equal("Section", options.Section.Key); + Assert.Equal("Section", options.Section.Path); + Assert.Null(options.Section.Value); + } + + [Fact] + public void CanBindWithKeyOverload() + { + var dic = new Dictionary + { + {"Section:Integer", "-2"}, + {"Section:Boolean", "TRUe"}, + {"Section:Nested:Integer", "11"}, + {"Section:Virtual", "Sup"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = new DerivedOptions(); + config.Bind("Section", options); + + Assert.True(options.Boolean); + Assert.Equal(-2, options.Integer); + Assert.Equal(11, options.Nested.Integer); + Assert.Equal("Derived:Sup", options.Virtual); + } + + [Fact] + public void CanBindIConfigurationSectionWithDerivedOptionsSection() + { + var dic = new Dictionary + { + {"Section:Integer", "-2"}, + {"Section:Boolean", "TRUe"}, + {"Section:Nested:Integer", "11"}, + {"Section:Virtual", "Sup"}, + {"Section:DerivedSection:Nested:Integer", "11"}, + {"Section:DerivedSection:Virtual", "Sup"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + + var childOptions = options.Section.Get(); + + var childDerivedOptions = childOptions.DerivedSection.Get(); + + Assert.True(childOptions.Boolean); + Assert.Equal(-2, childOptions.Integer); + Assert.Equal(11, childOptions.Nested.Integer); + Assert.Equal("Derived:Sup", childOptions.Virtual); + Assert.Equal(11, childDerivedOptions.Nested.Integer); + Assert.Equal("Derived:Sup", childDerivedOptions.Virtual); + + Assert.Equal("Section", options.Section.Key); + Assert.Equal("Section", options.Section.Path); + Assert.Equal("DerivedSection", childOptions.DerivedSection.Key); + Assert.Equal("Section:DerivedSection", childOptions.DerivedSection.Path); + Assert.Null(options.Section.Value); + } + + [Fact] + public void CanBindConfigurationKeyNameAttributes() + { + var dic = new Dictionary + { + {"Named_Property", "Yo"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + + Assert.Equal("Yo", options.NamedProperty); + } + + [Fact] + public void EmptyStringIsNullable() + { + var dic = new Dictionary + { + {"empty", ""}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + Assert.Null(config.GetValue("empty")); + Assert.Null(config.GetValue("empty")); + } + + [Fact] + public void GetScalarNullable() + { + 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")); + } + + [Fact] + public void CanBindToObjectProperty() + { + var dic = new Dictionary + { + {"Object", "whatever" } + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = new ComplexOptions(); + config.Bind(options); + + Assert.Equal("whatever", options.Object); + } + + [Fact] + public void GetNullValue() + { + var dic = new Dictionary + { + {"Integer", null}, + {"Boolean", null}, + {"Nested:Integer", null}, + {"Object", null } + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + Assert.False(config.GetValue("Boolean")); + Assert.Equal(0, config.GetValue("Integer")); + Assert.Equal(0, config.GetValue("Nested:Integer")); + Assert.Null(config.GetValue("Object")); + Assert.False(config.GetSection("Boolean").Get()); + Assert.Equal(0, config.GetSection("Integer").Get()); + Assert.Equal(0, config.GetSection("Nested:Integer").Get()); + Assert.Null(config.GetSection("Object").Get()); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need to honor binder options. + public void ThrowsIfPropertyInConfigMissingInModel() + { + var dic = new Dictionary + { + {"ThisDoesNotExistInTheModel", "42"}, + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var instance = new ComplexOptions(); + + var ex = Assert.Throws( + () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); + + string expectedMessage = SR.Format(SR.Error_MissingConfig, + nameof(BinderOptions.ErrorOnUnknownConfiguration), nameof(BinderOptions), typeof(ComplexOptions), "'ThisDoesNotExistInTheModel'"); + + Assert.Equal(expectedMessage, ex.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need to honor binder options. + public void ThrowsIfPropertyInConfigMissingInNestedModel() + { + var dic = new Dictionary + { + {"Nested:ThisDoesNotExistInTheModel", "42"}, + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var instance = new ComplexOptions(); + + string expectedMessage = SR.Format(SR.Error_MissingConfig, + nameof(BinderOptions.ErrorOnUnknownConfiguration), nameof(BinderOptions), typeof(NestedOptions), "'ThisDoesNotExistInTheModel'"); + + var ex = Assert.Throws( + () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); + + Assert.Equal(expectedMessage, ex.Message); + } + + [Fact] + public void GetDefaultsWhenDataDoesNotExist() + { + var dic = new Dictionary + { + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + Assert.False(config.GetValue("Boolean")); + Assert.Equal(0, config.GetValue("Integer")); + Assert.Equal(0, config.GetValue("Nested:Integer")); + Assert.Null(config.GetValue("Object")); + Assert.True(config.GetValue("Boolean", true)); + Assert.Equal(3, config.GetValue("Integer", 3)); + Assert.Equal(1, config.GetValue("Nested:Integer", 1)); + var foo = new ComplexOptions(); + Assert.Same(config.GetValue("Object", foo), foo); + } + + [Fact] + public void GetUri() + { + var dic = new Dictionary + { + {"AnUri", "http://www.bing.com"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var uri = config.GetValue("AnUri"); + + Assert.Equal("http://www.bing.com", uri.OriginalString); + } + + [Theory] + [InlineData("2147483647", typeof(int))] + [InlineData("4294967295", typeof(uint))] + [InlineData("32767", typeof(short))] + [InlineData("65535", typeof(ushort))] + [InlineData("-9223372036854775808", typeof(long))] + [InlineData("18446744073709551615", typeof(ulong))] + [InlineData("trUE", typeof(bool))] + [InlineData("255", typeof(byte))] + [InlineData("127", typeof(sbyte))] + [InlineData("\uffff", typeof(char))] + [InlineData("79228162514264337593543950335", typeof(decimal))] + [InlineData("1.79769e+308", typeof(double))] + [InlineData("3.40282347E+38", typeof(float))] + [InlineData("2015-12-24T07:34:42-5:00", typeof(DateTime))] + [InlineData("12/24/2015 13:44:55 +4", typeof(DateTimeOffset))] + [InlineData("99.22:22:22.1234567", typeof(TimeSpan))] + [InlineData("http://www.bing.com", typeof(Uri))] + // enum test + [InlineData("Constructor", typeof(AttributeTargets))] + [InlineData("CA761232-ED42-11CE-BACD-00AA0057B223", typeof(Guid))] + public void CanReadAllSupportedTypes(string value, Type type) + { + // arrange + var dic = new Dictionary + { + {"Value", value} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var optionsType = typeof(GenericOptions<>).MakeGenericType(type); + var options = Activator.CreateInstance(optionsType); + var expectedValue = TypeDescriptor.GetConverter(type).ConvertFromInvariantString(value); + + // act + config.Bind(options); + var optionsValue = options.GetType().GetProperty("Value").GetValue(options); + var getValueValue = config.GetValue(type, "Value"); + var getValue = config.GetSection("Value").Get(type); + + // assert + Assert.Equal(expectedValue, optionsValue); + Assert.Equal(expectedValue, getValue); + Assert.Equal(expectedValue, getValueValue); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(uint))] + [InlineData(typeof(short))] + [InlineData(typeof(ushort))] + [InlineData(typeof(long))] + [InlineData(typeof(ulong))] + [InlineData(typeof(bool))] + [InlineData(typeof(byte))] + [InlineData(typeof(sbyte))] + [InlineData(typeof(char))] + [InlineData(typeof(decimal))] + [InlineData(typeof(double))] + [InlineData(typeof(float))] + [InlineData(typeof(DateTime))] + [InlineData(typeof(DateTimeOffset))] + [InlineData(typeof(TimeSpan))] + [InlineData(typeof(AttributeTargets))] + [InlineData(typeof(Guid))] + public void ConsistentExceptionOnFailedBinding(Type type) + { + // arrange + const string IncorrectValue = "Invalid data"; + const string ConfigKey = "Value"; + var dic = new Dictionary + { + {ConfigKey, IncorrectValue} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var optionsType = typeof(GenericOptions<>).MakeGenericType(type); + var options = Activator.CreateInstance(optionsType); + + // act + var exception = Assert.Throws( + () => config.Bind(options)); + + var getValueException = Assert.Throws( + () => config.GetValue(type, "Value")); + + var getException = Assert.Throws( + () => config.GetSection("Value").Get(type)); + + // assert + Assert.NotNull(exception.InnerException); + Assert.NotNull(getException.InnerException); + Assert.Equal( + SR.Format(SR.Error_FailedBinding, ConfigKey, type), + exception.Message); + Assert.Equal( + SR.Format(SR.Error_FailedBinding, ConfigKey, type), + getException.Message); + Assert.Equal( + SR.Format(SR.Error_FailedBinding, ConfigKey, type), + getValueException.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync + public void ExceptionOnFailedBindingIncludesPath() + { + const string IncorrectValue = "Invalid data"; + const string ConfigKey = "Nested:Value"; + + var dic = new Dictionary + { + {ConfigKey, IncorrectValue} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = new OptionsWithNesting(); + + var exception = Assert.Throws( + () => config.Bind(options)); + + Assert.Equal(SR.Format(SR.Error_FailedBinding, ConfigKey, typeof(int)), + exception.Message); + } + + [Fact] + public void BinderIgnoresIndexerProperties() + { + var configurationBuilder = new ConfigurationBuilder(); + var config = configurationBuilder.Build(); + config.Bind(new List()); + } + + [Fact] + public void BindCanReadComplexProperties() + { + var dic = new Dictionary + { + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var instance = new ComplexOptions(); + config.Bind(instance); + + Assert.True(instance.Boolean); + Assert.Equal(-2, instance.Integer); + Assert.Equal(11, instance.Nested.Integer); + } + + [Fact] + public void GetCanReadComplexProperties() + { + var dic = new Dictionary + { + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = new ComplexOptions(); + config.Bind(options); + + Assert.True(options.Boolean); + Assert.Equal(-2, options.Integer); + Assert.Equal(11, options.Nested.Integer); + } + + [Fact] + public void BindCanReadInheritedProperties() + { + var dic = new Dictionary + { + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"}, + {"Virtual", "Sup"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var instance = new DerivedOptions(); + config.Bind(instance); + + Assert.True(instance.Boolean); + Assert.Equal(-2, instance.Integer); + Assert.Equal(11, instance.Nested.Integer); + Assert.Equal("Derived:Sup", instance.Virtual); + } + + [Fact] + public void GetCanReadInheritedProperties() + { + var dic = new Dictionary + { + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"}, + {"Virtual", "Sup"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = new DerivedOptions(); + config.Bind(options); + + Assert.True(options.Boolean); + Assert.Equal(-2, options.Integer); + Assert.Equal(11, options.Nested.Integer); + Assert.Equal("Derived:Sup", options.Virtual); + } + + [Fact] + public void GetCanReadStaticProperty() + { + var dic = new Dictionary + { + {"StaticProperty", "stuff"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + var options = new ComplexOptions(); + config.Bind(options); + + Assert.Equal("stuff", ComplexOptions.StaticProperty); + } + + [Fact] + public void BindCanReadStaticProperty() + { + var dic = new Dictionary + { + {"StaticProperty", "other stuff"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var instance = new ComplexOptions(); + config.Bind(instance); + + Assert.Equal("other stuff", ComplexOptions.StaticProperty); + } + + [Fact] + public void CanGetComplexOptionsWhichHasAlsoHasValue() + { + var dic = new Dictionary + { + {"obj", "whut" }, + {"obj:Integer", "-2"}, + {"obj:Boolean", "TRUe"}, + {"obj:Nested:Integer", "11"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.GetSection("obj").Get(); + Assert.NotNull(options); + Assert.True(options.Boolean); + Assert.Equal(-2, options.Integer); + Assert.Equal(11, options.Nested.Integer); + } + + [Theory] + [InlineData("ReadOnly")] + [InlineData("PrivateSetter")] + [InlineData("ProtectedSetter")] + [InlineData("InternalSetter")] + [InlineData("InternalProperty")] + [InlineData("PrivateProperty")] + [InlineData("ProtectedProperty")] + [InlineData("ProtectedPrivateSet")] + public void GetIgnoresTests(string property) + { + var dic = new Dictionary + { + {property, "stuff"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Null(options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); + } + + [Theory] + [InlineData("PrivateSetter")] + [InlineData("ProtectedSetter")] + [InlineData("InternalSetter")] + [InlineData("InternalProperty")] + [InlineData("PrivateProperty")] + [InlineData("ProtectedProperty")] + [InlineData("ProtectedPrivateSet")] + public void GetCanSetNonPublicWhenSet(string property) + { + var dic = new Dictionary + { + {property, "stuff"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(o => o.BindNonPublicProperties = true); + Assert.Equal("stuff", options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); + } + + [Theory] + [InlineData("InternalReadOnly")] + [InlineData("PrivateReadOnly")] + [InlineData("ProtectedReadOnly")] + public void NonPublicModeGetStillIgnoresReadonly(string property) + { + var dic = new Dictionary + { + {property, "stuff"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(o => o.BindNonPublicProperties = true); + Assert.Null(options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); + } + + [Theory] + [InlineData("ReadOnly")] + [InlineData("PrivateSetter")] + [InlineData("ProtectedSetter")] + [InlineData("InternalSetter")] + [InlineData("InternalProperty")] + [InlineData("PrivateProperty")] + [InlineData("ProtectedProperty")] + [InlineData("ProtectedPrivateSet")] + public void BindIgnoresTests(string property) + { + var dic = new Dictionary + { + {property, "stuff"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = new ComplexOptions(); + config.Bind(options); + + Assert.Null(options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); + } + + [Theory] + [InlineData("PrivateSetter")] + [InlineData("ProtectedSetter")] + [InlineData("InternalSetter")] + [InlineData("InternalProperty")] + [InlineData("PrivateProperty")] + [InlineData("ProtectedProperty")] + [InlineData("ProtectedPrivateSet")] + public void BindCanSetNonPublicWhenSet(string property) + { + var dic = new Dictionary + { + {property, "stuff"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = new ComplexOptions(); + config.Bind(options, o => o.BindNonPublicProperties = true); + Assert.Equal("stuff", options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); + } + + [Theory] + [InlineData("InternalReadOnly")] + [InlineData("PrivateReadOnly")] + [InlineData("ProtectedReadOnly")] + public void NonPublicModeBindStillIgnoresReadonly(string property) + { + var dic = new Dictionary + { + {property, "stuff"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = new ComplexOptions(); + config.Bind(options, o => o.BindNonPublicProperties = true); + Assert.Null(options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync + public void ExceptionWhenTryingToBindToInterface() + { + var input = new Dictionary + { + {"ISomeInterfaceProperty:Subkey", "x"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ISomeInterface)), + exception.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync + public void ExceptionWhenTryingToBindClassWithoutParameterlessConstructor() + { + var input = new Dictionary + { + {"ClassWithoutPublicConstructorProperty:Subkey", "x"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.Equal( + SR.Format(SR.Error_MissingPublicInstanceConstructor, typeof(ClassWithoutPublicConstructor)), + exception.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParameters() + { + var input = new Dictionary + { + {"ClassWhereParametersDoNotMatchPropertiesProperty:Name", "John"}, + {"ClassWhereParametersDoNotMatchPropertiesProperty:Address", "123, Abc St."}, + {"ClassWhereParametersDoNotMatchPropertiesProperty:Age", "42"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.Equal( + SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, typeof(ClassWhereParametersDoNotMatchProperties), "age"), + exception.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() // Need support for parameterized ctors. + { + var input = new Dictionary + { + {"LineProperty:Color", "Red"}, + {"LineProperty:Length", "22"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.Equal( + SR.Format(SR.Error_ParameterHasNoMatchingConfig, typeof(Line), nameof(Line.Thickness)), + exception.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor() // Need support for parameterized ctors. + { + var input = new Dictionary + { + {"ClassWhereParametersDoNotMatchPropertiesProperty:Name", "John"}, + {"ClassWhereParametersDoNotMatchPropertiesProperty:Address", "123, Abc St."}, + {"ClassWhereParametersDoNotMatchPropertiesProperty:Age", "42"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.Equal( + SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, typeof(ClassWhereParametersDoNotMatchProperties), "age"), + exception.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + public void BindsToClassConstructorParametersWithDefaultValues() // Need support for parameterized ctors. + { + var input = new Dictionary + { + {"ClassWhereParametersHaveDefaultValueProperty:Name", "John"}, + {"ClassWhereParametersHaveDefaultValueProperty:Address", "123, Abc St."} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + TestOptions testOptions = new TestOptions(); + + config.Bind(testOptions); + Assert.Equal("John", testOptions.ClassWhereParametersHaveDefaultValueProperty.Name); + Assert.Equal("123, Abc St.", testOptions.ClassWhereParametersHaveDefaultValueProperty.Address); + Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.Age); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void FieldsNotSupported_ExceptionBindingToConstructorWithParameterMatchingAField() + { + var input = new Dictionary + { + {"ClassWhereParametersMatchPropertiesAndFieldsProperty:Name", "John"}, + {"ClassWhereParametersMatchPropertiesAndFieldsProperty:Address", "123, Abc St."}, + {"ClassWhereParametersMatchPropertiesAndFieldsProperty:Age", "42"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + + Assert.Equal( + SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, typeof(ClassWhereParametersMatchPropertiesAndFields), "age"), + exception.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void BindsToRecordPrimaryConstructorParametersWithDefaultValues() + { + var input = new Dictionary + { + {"RecordWhereParametersHaveDefaultValueProperty:Name", "John"}, + {"RecordWhereParametersHaveDefaultValueProperty:Address", "123, Abc St."} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + TestOptions testOptions = new TestOptions(); + + config.Bind(testOptions); + Assert.Equal("John", testOptions.RecordWhereParametersHaveDefaultValueProperty.Name); + Assert.Equal("123, Abc St.", testOptions.RecordWhereParametersHaveDefaultValueProperty.Address); + Assert.Equal(42, testOptions.RecordWhereParametersHaveDefaultValueProperty.Age); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync + public void ExceptionWhenTryingToBindToTypeThrowsWhenActivated() + { + var input = new Dictionary + { + {"ThrowsWhenActivatedProperty:subkey", "x"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.NotNull(exception.InnerException); + Assert.Equal( + SR.Format(SR.Error_FailedToActivate, typeof(ThrowsWhenActivated)), + exception.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync + public void ExceptionIncludesKeyOfFailedBinding() + { + var input = new Dictionary + { + {"NestedOptionsProperty:NestedOptions2Property:ISomeInterfaceProperty:subkey", "x"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ISomeInterface)), + exception.Message); + } + + [Fact] + public void CanBindValueTypeOptions() + { + var dic = new Dictionary + { + {"MyInt32", "42"}, + {"MyString", "hello world"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + var options = config.Get(); + Assert.Equal(42, options.MyInt32); + Assert.Equal("hello world", options.MyString); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindImmutableClass() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindMutableClassWitNestedImmutableObject() + { + var dic = new Dictionary + { + {"ContainerName", "Container123"}, + {"LengthAndColor:Length", "42"}, + {"LengthAndColor:Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal("Container123", options.ContainerName); + Assert.Equal(42, options.LengthAndColor.Length); + Assert.Equal("Green", options.LengthAndColor.Color); + } + + // If the immutable type has multiple public parameterized constructors, then throw + // an exception. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindImmutableClass_ThrowsOnMultipleParameterizedConstructors() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + string expectedMessage = SR.Format(SR.Error_MultipleParameterizedConstructors, "Microsoft.Extensions.Configuration.Binder.Tests.ConfigurationBinderTests+ImmutableClassWithMultipleParameterizedConstructors"); + + var ex = Assert.Throws(() => config.Get()); + + Assert.Equal(expectedMessage, ex.Message); + } + + // If the immutable type has a parameterized constructor, then throw + // that constructor has an 'in' parameter + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnInParameter() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Tests.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithInParameter", "string1"); + + var ex = Assert.Throws(() => config.Get()); + + Assert.Equal(expectedMessage, ex.Message); + } + + // If the immutable type has a parameterized constructors, then throw + // that constructor has a 'ref' parameter + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithARefParameter() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Tests.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithRefParameter", "int1"); + + var ex = Assert.Throws(() => config.Get()); + + Assert.Equal(expectedMessage, ex.Message); + } + + // If the immutable type has a parameterized constructors, then throw + // if the constructor has an 'out' parameter + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnOutParameter() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Tests.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithOutParameter", "int2"); + + var ex = Assert.Throws(() => config.Get()); + + Assert.Equal(expectedMessage, ex.Message); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindMutableStruct_UnmatchedConstructorsAreIgnored() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + + // If the immutable type has a public parameterized constructor, + // then pick it. + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindImmutableClass_PicksParameterizedConstructorIfNoParameterlessConstructorExists() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal("s1", options.String1); + Assert.Equal("s2", options.String2); + Assert.Equal(1, options.Int1); + Assert.Equal(2, options.Int2); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindSemiImmutableClass() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + {"Thickness", "1.23"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + Assert.Equal(1.23m, options.Thickness); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindSemiImmutableClass_WithInitProperties() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + {"Thickness", "1.23"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + Assert.Equal(1.23m, options.Thickness); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindRecordOptions() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindRecordStructOptions() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindNestedRecordOptions() + { + var dic = new Dictionary + { + {"Number", "1"}, + {"Nested1:ValueA", "Cool"}, + {"Nested1:ValueB", "42"}, + {"Nested2:ValueA", "Uncool"}, + {"Nested2:ValueB", "24"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(1, options.Number); + Assert.Equal("Cool", options.Nested1.ValueA); + Assert.Equal(42, options.Nested1.ValueB); + Assert.Equal("Uncool", options.Nested2.ValueA); + Assert.Equal(24, options.Nested2.ValueB); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindOnParametersAndProperties_PropertiesAreSetAfterTheConstructor() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("the color is Green", options.Color); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors. + public void CanBindReadonlyRecordStructOptions() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + + [Fact] + public void CanBindByteArray() + { + var bytes = new byte[] { 1, 2, 3, 4 }; + var dic = new Dictionary + { + { "MyByteArray", Convert.ToBase64String(bytes) } + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(bytes, options.MyByteArray); + } + + [Fact] + public void CanBindByteArrayWhenValueIsNull() + { + var dic = new Dictionary + { + { "MyByteArray", null } + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Null(options.MyByteArray); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync + public void ExceptionWhenTryingToBindToByteArray() + { + var dic = new Dictionary + { + { "MyByteArray", "(not a valid base64 string)" } + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_FailedBinding, "MyByteArray", typeof(byte[])), + exception.Message); + } + + [Fact] + public void DoesNotReadPropertiesUnnecessarily() + { + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + { nameof(ClassWithReadOnlyPropertyThatThrows.Safe), "value" }, + { nameof(ClassWithReadOnlyPropertyThatThrows.StringThrows), "value" }, + { $"{nameof(ClassWithReadOnlyPropertyThatThrows.EnumerableThrows)}:0", "0" }, + }); + IConfiguration config = configurationBuilder.Build(); + + ClassWithReadOnlyPropertyThatThrows bound = config.Get(); + Assert.Equal("value", bound.Safe); + } + + /// + /// Binding to mutable structs is important to support properties + /// like JsonConsoleFormatterOptions.JsonWriterOptions. + /// + [Fact] + public void CanBindNestedStructProperties() + { + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + { "ReadWriteNestedStruct:String", "s" }, + { "ReadWriteNestedStruct:DeeplyNested:Int32", "100" }, + { "ReadWriteNestedStruct:DeeplyNested:Boolean", "true" }, + }); + IConfiguration config = configurationBuilder.Build(); + + StructWithNestedStructs bound = config.Get(); + Assert.Equal("s", bound.ReadWriteNestedStruct.String); + Assert.Equal(100, bound.ReadWriteNestedStruct.DeeplyNested.Int32); + Assert.True(bound.ReadWriteNestedStruct.DeeplyNested.Boolean); + } + + [Fact] + public void IgnoresReadOnlyNestedStructProperties() + { + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + { "ReadOnlyNestedStruct:String", "s" }, + { "ReadOnlyNestedStruct:DeeplyNested:Int32", "100" }, + { "ReadOnlyNestedStruct:DeeplyNested:Boolean", "true" }, + }); + IConfiguration config = configurationBuilder.Build(); + + StructWithNestedStructs bound = config.Get(); + Assert.Null(bound.ReadOnlyNestedStruct.String); + Assert.Equal(0, bound.ReadWriteNestedStruct.DeeplyNested.Int32); + Assert.False(bound.ReadWriteNestedStruct.DeeplyNested.Boolean); + } + + [Fact] + public void CanBindNullableNestedStructProperties() + { + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + { "NullableNestedStruct:String", "s" }, + { "NullableNestedStruct:DeeplyNested:Int32", "100" }, + { "NullableNestedStruct:DeeplyNested:Boolean", "true" }, + }); + IConfiguration config = configurationBuilder.Build(); + StructWithNestedStructs bound = config.Get(); + Assert.NotNull(bound.NullableNestedStruct); + Assert.Equal("s", bound.NullableNestedStruct.Value.String); + Assert.Equal(100, bound.NullableNestedStruct.Value.DeeplyNested.Int32); + Assert.True(bound.NullableNestedStruct.Value.DeeplyNested.Boolean); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need collection support. + public void CanBindVirtualProperties() + { + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + { $"{nameof(BaseClassWithVirtualProperty.Test)}:0", "1" }, + { $"{nameof(BaseClassWithVirtualProperty.TestGetSetOverridden)}", "2" }, + { $"{nameof(BaseClassWithVirtualProperty.TestGetOverridden)}", "3" }, + { $"{nameof(BaseClassWithVirtualProperty.TestSetOverridden)}", "4" }, + { $"{nameof(BaseClassWithVirtualProperty.TestNoOverridden)}", "5" }, + { $"{nameof(BaseClassWithVirtualProperty.TestVirtualSet)}", "6" } + }); + IConfiguration config = configurationBuilder.Build(); + + var test = new ClassOverridingVirtualProperty(); + config.Bind(test); + + Assert.Equal("1", Assert.Single(test.Test)); + Assert.Equal("2", test.TestGetSetOverridden); + Assert.Equal("3", test.TestGetOverridden); + Assert.Equal("4", test.TestSetOverridden); + Assert.Equal("5", test.TestNoOverridden); + Assert.Null(test.ExposeTestVirtualSet()); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need to honor binder options. + public void CanBindPrivatePropertiesFromBaseClass() + { + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + { "PrivateProperty", "a" } + }); + IConfiguration config = configurationBuilder.Build(); + + var test = new ClassOverridingVirtualProperty(); + config.Bind(test, b => b.BindNonPublicProperties = true); + Assert.Equal("a", test.ExposePrivatePropertyValue()); + } + + [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need collection support. + public void EnsureCallingThePropertySetter() + { + var json = @"{ + ""IPFiltering"": { + ""HttpStatusCode"": 401, + ""Blacklist"": [ ""192.168.0.10-192.168.10.20"", ""fe80::/10"" ] + } + }"; + + var configuration = new ConfigurationBuilder() + .AddJsonStream(TestStreamHelpers.StringToStream(json)) + .Build(); + + OptionWithCollectionProperties options = configuration.GetSection("IPFiltering").Get(); + + Assert.NotNull(options); + Assert.Equal(2, options.Blacklist.Count); + Assert.Equal("192.168.0.10-192.168.10.20", options.Blacklist.ElementAt(0)); + Assert.Equal("fe80::/10", options.Blacklist.ElementAt(1)); + + Assert.Equal(2, options.ParsedBlacklist.Count); // should be initialized when calling the options.Blacklist setter. + + Assert.Equal(401, options.HttpStatusCode); // exists in configuration and properly sets the property + Assert.Equal(2, options.OtherCode); // doesn't exist in configuration. the setter sets default value '2' + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs deleted file mode 100644 index 0242fda851536..0000000000000 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ /dev/null @@ -1,2911 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Test; -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Reflection; -using Xunit; - -namespace Microsoft.Extensions.Configuration.Binder.Test -{ - public class ConfigurationBinderTests - { - public class ComplexOptions - { - private static Dictionary _existingDictionary = new() - { - {"existing-item1", 1}, - {"existing-item2", 2}, - }; - - public ComplexOptions() - { - Nested = new NestedOptions(); - Virtual = "complex"; - } - - public NestedOptions Nested { get; set; } - public int Integer { get; set; } - public bool Boolean { get; set; } - public virtual string Virtual { get; set; } - public object Object { get; set; } - - public string PrivateSetter { get; private set; } - public string ProtectedSetter { get; protected set; } - public string InternalSetter { get; internal set; } - public static string StaticProperty { get; set; } - - private string PrivateProperty { get; set; } - internal string InternalProperty { get; set; } - protected string ProtectedProperty { get; set; } - - [ConfigurationKeyName("Named_Property")] - public string NamedProperty { get; set; } - - protected string ProtectedPrivateSet { get; private set; } - - private string PrivateReadOnly { get; } - internal string InternalReadOnly { get; } - protected string ProtectedReadOnly { get; } - - public string ReadOnly - { - get { return null; } - } - - public ISet NonInstantiatedISet { get; set; } = null!; - public HashSet NonInstantiatedHashSet { get; set; } = null!; - public IDictionary> NonInstantiatedDictionaryWithISet { get; set; } = null!; - public IDictionary> InstantiatedDictionaryWithHashSet { get; set; } = - new Dictionary>(); - - public IDictionary> InstantiatedDictionaryWithHashSetWithSomeValues { get; set; } = - new Dictionary> - { - {"item1", new HashSet(new[] {"existing1", "existing2"})} - }; - - public IEnumerable NonInstantiatedIEnumerable { get; set; } = null!; - - public ISet InstantiatedISet { get; set; } = new HashSet(); - - public ISet ISetNoSetter { get; } = new HashSet(); - - public HashSet InstantiatedHashSetWithSomeValues { get; set; } = - new HashSet(new[] { "existing1", "existing2" }); - - public SortedSet InstantiatedSortedSetWithSomeValues { get; set; } = - new SortedSet(new[] { "existing1", "existing2" }); - - public SortedSet NonInstantiatedSortedSetWithSomeValues { get; set; } = null!; - - public ISet InstantiatedISetWithSomeValues { get; set; } = - new HashSet(new[] { "existing1", "existing2" }); - - public ISet HashSetWithUnsupportedKey { get; set; } = - new HashSet(); - - public ISet UninstantiatedHashSetWithUnsupportedKey { get; set; } - -#if NETCOREAPP - public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = new HashSet(); - public IReadOnlySet InstantiatedIReadOnlySetWithSomeValues { get; set; } = - new HashSet(new[] { "existing1", "existing2" }); - public IReadOnlySet NonInstantiatedIReadOnlySet { get; set; } - public IDictionary> InstantiatedDictionaryWithReadOnlySetWithSomeValues { get; set; } = - new Dictionary> - { - {"item1", new HashSet(new[] {"existing1", "existing2"})} - }; -#endif - public IReadOnlyDictionary InstantiatedReadOnlyDictionaryWithWithSomeValues { get; set; } = - _existingDictionary; - - public IReadOnlyDictionary NonInstantiatedReadOnlyDictionary { get; set; } - - public CustomICollectionWithoutAnAddMethod InstantiatedCustomICollectionWithoutAnAddMethod { get; set; } = new(); - public CustomICollectionWithoutAnAddMethod NonInstantiatedCustomICollectionWithoutAnAddMethod { get; set; } - - public IEnumerable InstantiatedIEnumerable { get; set; } = new List(); - public ICollection InstantiatedICollection { get; set; } = new List(); - public IReadOnlyCollection InstantiatedIReadOnlyCollection { get; set; } = new List(); - } - - public class NestedOptions - { - public int Integer { get; set; } - } - - public class DerivedOptions : ComplexOptions - { - public override string Virtual - { - get - { - return base.Virtual; - } - set - { - base.Virtual = "Derived:" + value; - } - } - } - - public class UnsupportedTypeInHashSet { } - - public interface ICustomCollectionDerivedFromIEnumerableT : IEnumerable { } - public interface ICustomCollectionDerivedFromICollectionT : ICollection { } - - public class MyClassWithCustomCollections - { - public ICustomCollectionDerivedFromIEnumerableT CustomIEnumerableCollection { get; set; } - public ICustomCollectionDerivedFromICollectionT CustomCollection { get; set; } - } - - public class CustomICollectionWithoutAnAddMethod : ICollection - { - private readonly List _items = new(); - public IEnumerator GetEnumerator() => _items.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - void ICollection.Add(string item) => _items.Add(item); - - public void Clear() => _items.Clear(); - - public bool Contains(string item) => _items.Contains(item); - - public void CopyTo(string[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex); - - public bool Remove(string item) => _items.Remove(item); - - public int Count => _items.Count; - public bool IsReadOnly => false; - } - - public interface ICustomSet : ISet - { - } - - public class MyClassWithCustomSet - { - public ICustomSet CustomSet { get; set; } - } - - public class MyClassWithCustomDictionary - { - public ICustomDictionary CustomDictionary { get; set; } - } - - public class ConfigWithInstantiatedIReadOnlyDictionary - { - public static Dictionary _existingDictionary = new() - { - {"existing-item1", 1}, - {"existing-item2", 2}, - }; - - public IReadOnlyDictionary Dictionary { get; set; } = - _existingDictionary; - } - - public class ConfigWithNonInstantiatedReadOnlyDictionary - { - public IReadOnlyDictionary Dictionary { get; set; } = null!; - } - - public class ConfigWithInstantiatedConcreteDictionary - { - public static Dictionary _existingDictionary = new() - { - {"existing-item1", 1}, - {"existing-item2", 2}, - }; - - public Dictionary Dictionary { get; set; } = - _existingDictionary; - } - - public interface ICustomDictionary : IDictionary - { - } - - public class NullableOptions - { - public bool? MyNullableBool { get; set; } - public int? MyNullableInt { get; set; } - public DateTime? MyNullableDateTime { get; set; } - } - - public class EnumOptions - { - public UriKind UriKind { get; set; } - } - - public class GenericOptions - { - public T Value { get; set; } - } - - public class OptionsWithNesting - { - public NestedOptions Nested { get; set; } - - public class NestedOptions - { - public int Value { get; set; } - } - } - - public class ConfigurationInterfaceOptions - { - public IConfigurationSection Section { get; set; } - } - - public class DerivedOptionsWithIConfigurationSection : DerivedOptions - { - public IConfigurationSection DerivedSection { get; set; } - } - - public record struct RecordStructTypeOptions(string Color, int Length); - - public record RecordOptionsWithNesting(int Number, RecordOptionsWithNesting.RecordNestedOptions Nested1, - RecordOptionsWithNesting.RecordNestedOptions Nested2 = null!) - { - public record RecordNestedOptions(string ValueA, int ValueB); - } - - // Here, the constructor has three parameters, but not all of those match - // match to a property or field - public class ClassWhereParametersDoNotMatchProperties - { - public string Name { get; } - public string Address { get; } - - public ClassWhereParametersDoNotMatchProperties(string name, string address, int age) - { - Name = name; - Address = address; - } - } - - // Here, the constructor has three parameters, and two of them match properties - // and one of them match a field. - public class ClassWhereParametersMatchPropertiesAndFields - { - private int Age; - - public string Name { get; } - public string Address { get; } - - public ClassWhereParametersMatchPropertiesAndFields(string name, string address, int age) - { - Name = name; - Address = address; - Age = age; - } - - public int GetAge() => Age; - } - - public record RecordWhereParametersHaveDefaultValue(string Name, string Address, int Age = 42); - - public record ClassWhereParametersHaveDefaultValue - { - public string? Name { get; } - public string Address { get; } - public int Age { get; } - - public ClassWhereParametersHaveDefaultValue(string? name, string address, int age = 42) - { - Name = name; - Address = address; - Age = age; - } - } - - - public record RecordTypeOptions(string Color, int Length); - - public record Line(string Color, int Length, int Thickness); - - public class ClassWithMatchingParametersAndProperties - { - private readonly string _color; - - public ClassWithMatchingParametersAndProperties(string Color, int Length) - { - _color = Color; - this.Length = Length; - } - - public int Length { get; set; } - - public string Color - { - get => _color; - init => _color = "the color is " + value; - } - } - - public readonly record struct ReadonlyRecordStructTypeOptions(string Color, int Length); - - public class ContainerWithNestedImmutableObject - { - public string ContainerName { get; set; } - public ImmutableLengthAndColorClass LengthAndColor { get; set; } - } - - public struct MutableStructWithConstructor - { - public MutableStructWithConstructor(string randomParameter) - { - Color = randomParameter; - Length = randomParameter.Length; - } - - public string Color { get; set; } - public int Length { get; set; } - } - - public class ImmutableLengthAndColorClass - { - public ImmutableLengthAndColorClass(string color, int length) - { - Color = color; - Length = length; - } - - public string Color { get; } - public int Length { get; } - } - - public class ImmutableClassWithOneParameterizedConstructor - { - public ImmutableClassWithOneParameterizedConstructor(string string1, int int1, string string2, int int2) - { - String1 = string1; - Int1 = int1; - String2 = string2; - Int2 = int2; - } - - public string String1 { get; } - public string String2 { get; } - public int Int1 { get; } - public int Int2 { get; } - } - - public class ImmutableClassWithOneParameterizedConstructorButWithInParameter - { - public ImmutableClassWithOneParameterizedConstructorButWithInParameter(in string string1, int int1, string string2, int int2) - { - String1 = string1; - Int1 = int1; - String2 = string2; - Int2 = int2; - } - - public string String1 { get; } - public string String2 { get; } - public int Int1 { get; } - public int Int2 { get; } - } - - public class ImmutableClassWithOneParameterizedConstructorButWithRefParameter - { - public ImmutableClassWithOneParameterizedConstructorButWithRefParameter(string string1, ref int int1, string string2, int int2) - { - String1 = string1; - Int1 = int1; - String2 = string2; - Int2 = int2; - } - - public string String1 { get; } - public string String2 { get; } - public int Int1 { get; } - public int Int2 { get; } - } - - public class ImmutableClassWithOneParameterizedConstructorButWithOutParameter - { - public ImmutableClassWithOneParameterizedConstructorButWithOutParameter(string string1, int int1, - string string2, out decimal int2) - { - String1 = string1; - Int1 = int1; - String2 = string2; - int2 = 0; - } - - public string String1 { get; } - public string String2 { get; } - public int Int1 { get; } - public int Int2 { get; } - } - - public class ImmutableClassWithMultipleParameterizedConstructors - { - public ImmutableClassWithMultipleParameterizedConstructors(string string1, int int1) - { - String1 = string1; - Int1 = int1; - } - - public ImmutableClassWithMultipleParameterizedConstructors(string string1, int int1, string string2) - { - String1 = string1; - Int1 = int1; - String2 = string2; - } - - public ImmutableClassWithMultipleParameterizedConstructors(string string1, int int1, string string2, int int2) - { - String1 = string1; - Int1 = int1; - String2 = string2; - Int2 = int2; - } - - public ImmutableClassWithMultipleParameterizedConstructors(string string1) - { - String1 = string1; - } - - public string String1 { get; } - public string String2 { get; } - public int Int1 { get; } - public int Int2 { get; } - } - - public class SemiImmutableClass - { - public SemiImmutableClass(string color, int length) - { - Color = color; - Length = length; - } - - public string Color { get; } - public int Length { get; } - public decimal Thickness { get; set; } - } - - public class SemiImmutableClassWithInit - { - public SemiImmutableClassWithInit(string color, int length) - { - Color = color; - Length = length; - } - - public string Color { get; } - public int Length { get; } - public decimal Thickness { get; init; } - } - - public struct ValueTypeOptions - { - public int MyInt32 { get; set; } - public string MyString { get; set; } - } - - public class ByteArrayOptions - { - public byte[] MyByteArray { get; set; } - } - - public enum TestSettingsEnum - { - Option1, - Option2, - } - - public class CollectionsBindingWithErrorOnUnknownConfiguration - { - public class MyModelContainingArray - { - public TestSettingsEnum[] Enums { get; set; } - } - - public class MyModelContainingADictionary - { - public Dictionary Enums { get; set; } - } - - [Fact] - public void WithFlagUnset_NoExceptionIsThrownWhenFailingToParseEnumsInAnArrayAndValidItemsArePreserved() - { - var dic = new Dictionary - { - {"Section:Enums:0", "Option1"}, - {"Section:Enums:1", "Option3"}, // invalid - ignored - {"Section:Enums:2", "Option4"}, // invalid - ignored - {"Section:Enums:3", "Option2"}, - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - var configSection = config.GetSection("Section"); - - var model = configSection.Get(o => o.ErrorOnUnknownConfiguration = false); - - Assert.Equal(2, model.Enums.Length); - Assert.Equal(TestSettingsEnum.Option1, model.Enums[0]); - Assert.Equal(TestSettingsEnum.Option2, model.Enums[1]); - } - - [Fact] - public void WithFlagUnset_NoExceptionIsThrownWhenFailingToParseEnumsInADictionaryAndValidItemsArePreserved() - { - var dic = new Dictionary - { - {"Section:Enums:First", "Option1"}, - {"Section:Enums:Second", "Option3"}, // invalid - ignored - {"Section:Enums:Third", "Option4"}, // invalid - ignored - {"Section:Enums:Fourth", "Option2"}, - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - var configSection = config.GetSection("Section"); - - var model = configSection.Get(o => - o.ErrorOnUnknownConfiguration = false); - - Assert.Equal(2, model.Enums.Count); - Assert.Equal(TestSettingsEnum.Option1, model.Enums["First"]); - Assert.Equal(TestSettingsEnum.Option2, model.Enums["Fourth"]); - } - - [Fact] - public void WithFlagSet_AnExceptionIsThrownWhenFailingToParseEnumsInAnArray() - { - var dic = new Dictionary - { - {"Section:Enums:0", "Option1"}, - {"Section:Enums:1", "Option3"}, // invalid - exception thrown - {"Section:Enums:2", "Option1"}, - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - var configSection = config.GetSection("Section"); - - var exception = Assert.Throws( - () => configSection.Get(o => o.ErrorOnUnknownConfiguration = true)); - - Assert.Equal( - SR.Format(SR.Error_GeneralErrorWhenBinding, nameof(BinderOptions.ErrorOnUnknownConfiguration)), - exception.Message); - } - - [Fact] - public void WithFlagSet_AnExceptionIsThrownWhenFailingToParseEnumsInADictionary() - { - var dic = new Dictionary - { - {"Section:Enums:First", "Option1"}, - {"Section:Enums:Second", "Option3"}, // invalid - exception thrown - {"Section:Enums:Third", "Option1"}, - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - var configSection = config.GetSection("Section"); - - var exception = Assert.Throws( - () => configSection.Get(o => - o.ErrorOnUnknownConfiguration = true)); - - Assert.Equal( - SR.Format(SR.Error_GeneralErrorWhenBinding, nameof(BinderOptions.ErrorOnUnknownConfiguration)), - exception.Message); - } - } - - public record RootConfig(NestedConfig Nested); - - public record NestedConfig(string MyProp); - - [Fact] - public void BindWithNestedTypesWithReadOnlyProperties() - { - IConfiguration configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - { "Nested:MyProp", "Dummy" } - }) - .Build(); - - var result = configuration.Get(); - - Assert.Equal("Dummy", result.Nested.MyProp); - } - - [Fact] - public void EnumBindCaseInsensitiveNotThrows() - { - var dic = new Dictionary - { - {"Section:Option1", "opt1"}, - {"Section:option2", "opt2"} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - var configSection = config.GetSection("Section"); - - var configOptions = new Dictionary(); - configSection.Bind(configOptions); - - Assert.Equal("opt1", configOptions[TestSettingsEnum.Option1]); - Assert.Equal("opt2", configOptions[TestSettingsEnum.Option2]); - } - - [Fact] - public void CanBindIConfigurationSection() - { - var dic = new Dictionary - { - {"Section:Integer", "-2"}, - {"Section:Boolean", "TRUe"}, - {"Section:Nested:Integer", "11"}, - {"Section:Virtual", "Sup"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - - var childOptions = options.Section.Get(); - - Assert.True(childOptions.Boolean); - Assert.Equal(-2, childOptions.Integer); - Assert.Equal(11, childOptions.Nested.Integer); - Assert.Equal("Derived:Sup", childOptions.Virtual); - - Assert.Equal("Section", options.Section.Key); - Assert.Equal("Section", options.Section.Path); - Assert.Null(options.Section.Value); - } - - [Fact] - public void CanBindWithKeyOverload() - { - var dic = new Dictionary - { - {"Section:Integer", "-2"}, - {"Section:Boolean", "TRUe"}, - {"Section:Nested:Integer", "11"}, - {"Section:Virtual", "Sup"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = new DerivedOptions(); - config.Bind("Section", options); - - Assert.True(options.Boolean); - Assert.Equal(-2, options.Integer); - Assert.Equal(11, options.Nested.Integer); - Assert.Equal("Derived:Sup", options.Virtual); - } - - [Fact] - public void CanBindIConfigurationSectionWithDerivedOptionsSection() - { - var dic = new Dictionary - { - {"Section:Integer", "-2"}, - {"Section:Boolean", "TRUe"}, - {"Section:Nested:Integer", "11"}, - {"Section:Virtual", "Sup"}, - {"Section:DerivedSection:Nested:Integer", "11"}, - {"Section:DerivedSection:Virtual", "Sup"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - - var childOptions = options.Section.Get(); - - var childDerivedOptions = childOptions.DerivedSection.Get(); - - Assert.True(childOptions.Boolean); - Assert.Equal(-2, childOptions.Integer); - Assert.Equal(11, childOptions.Nested.Integer); - Assert.Equal("Derived:Sup", childOptions.Virtual); - Assert.Equal(11, childDerivedOptions.Nested.Integer); - Assert.Equal("Derived:Sup", childDerivedOptions.Virtual); - - Assert.Equal("Section", options.Section.Key); - Assert.Equal("Section", options.Section.Path); - Assert.Equal("DerivedSection", childOptions.DerivedSection.Key); - Assert.Equal("Section:DerivedSection", childOptions.DerivedSection.Path); - Assert.Null(options.Section.Value); - } - - [Fact] - public void CanBindConfigurationKeyNameAttributes() - { - var dic = new Dictionary - { - {"Named_Property", "Yo"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - - Assert.Equal("Yo", options.NamedProperty); - } - - [Fact] - public void CanBindNonInstantiatedIEnumerableWithItems() - { - var dic = new Dictionary - { - {"NonInstantiatedIEnumerable:0", "Yo1"}, - {"NonInstantiatedIEnumerable:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.NonInstantiatedIEnumerable.Count()); - Assert.Equal("Yo1", options.NonInstantiatedIEnumerable.ElementAt(0)); - Assert.Equal("Yo2", options.NonInstantiatedIEnumerable.ElementAt(1)); - } - - [Fact] - public void CanBindNonInstantiatedISet() - { - var dic = new Dictionary - { - {"NonInstantiatedISet:0", "Yo1"}, - {"NonInstantiatedISet:1", "Yo2"}, - {"NonInstantiatedISet:2", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.NonInstantiatedISet.Count); - Assert.Equal("Yo1", options.NonInstantiatedISet.ElementAt(0)); - Assert.Equal("Yo2", options.NonInstantiatedISet.ElementAt(1)); - } - - [Fact] - public void CanBindISetNoSetter() - { - var dic = new Dictionary - { - {"ISetNoSetter:0", "Yo1"}, - {"ISetNoSetter:1", "Yo2"}, - {"ISetNoSetter:2", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get(o => o.ErrorOnUnknownConfiguration = true)!; - - Assert.Equal(2, options.ISetNoSetter.Count); - Assert.Equal("Yo1", options.ISetNoSetter.ElementAt(0)); - Assert.Equal("Yo2", options.ISetNoSetter.ElementAt(1)); - } - -#if NETCOREAPP - [Fact] - public void CanBindInstantiatedIReadOnlySet() - { - var dic = new Dictionary - { - {"InstantiatedIReadOnlySet:0", "Yo1"}, - {"InstantiatedIReadOnlySet:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.InstantiatedIReadOnlySet.Count); - Assert.Equal("Yo1", options.InstantiatedIReadOnlySet.ElementAt(0)); - Assert.Equal("Yo2", options.InstantiatedIReadOnlySet.ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedIReadOnlyWithSomeValues() - { - var dic = new Dictionary - { - {"InstantiatedIReadOnlySetWithSomeValues:0", "Yo1"}, - {"InstantiatedIReadOnlySetWithSomeValues:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(4, options.InstantiatedIReadOnlySetWithSomeValues.Count); - Assert.Equal("existing1", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(0)); - Assert.Equal("existing2", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(1)); - Assert.Equal("Yo1", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(2)); - Assert.Equal("Yo2", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(3)); - } - - [Fact] - public void CanBindNonInstantiatedIReadOnlySet() - { - var dic = new Dictionary - { - {"NonInstantiatedIReadOnlySet:0", "Yo1"}, - {"NonInstantiatedIReadOnlySet:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.NonInstantiatedIReadOnlySet.Count); - Assert.Equal("Yo1", options.NonInstantiatedIReadOnlySet.ElementAt(0)); - Assert.Equal("Yo2", options.NonInstantiatedIReadOnlySet.ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedDictionaryOfIReadOnlySetWithSomeExistingValues() - { - var dic = new Dictionary - { - {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:foo:0", "foo-1"}, - {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:foo:1", "foo-2"}, - {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:bar:0", "bar-1"}, - {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:bar:1", "bar-2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(3, options.InstantiatedDictionaryWithReadOnlySetWithSomeValues.Count); - Assert.Equal("existing1", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["item1"].ElementAt(0)); - Assert.Equal("existing2", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["item1"].ElementAt(1)); - - Assert.Equal("foo-1", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["foo"].ElementAt(0)); - Assert.Equal("foo-2", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["foo"].ElementAt(1)); - Assert.Equal("bar-1", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["bar"].ElementAt(0)); - Assert.Equal("bar-2", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["bar"].ElementAt(1)); - } -#endif - - public class Foo - { - public IReadOnlyDictionary Items { get; set; } = - new Dictionary { { "existing-item1", 1 }, { "existing-item2", 2 } }; - - } - - [Fact] - public void CanBindInstantiatedReadOnlyDictionary2() - { - var dic = new Dictionary - { - {"Items:item3", "3"}, - {"Items:item4", "4"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(4, options.Items.Count); - Assert.Equal(1, options.Items["existing-item1"]); - Assert.Equal(2, options.Items["existing-item2"]); - Assert.Equal(3, options.Items["item3"]); - Assert.Equal(4, options.Items["item4"]); - } - - [Fact] - public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() - { - var dic = new Dictionary - { - {"Dictionary:existing-item1", "666"}, - {"Dictionary:item3", "3"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - // Readonly dictionary with instantiated value cannot be mutated by the configuration - Assert.Equal(3, options.Dictionary.Count); - - // does not overwrite original - Assert.Equal(1, ConfigWithInstantiatedIReadOnlyDictionary._existingDictionary["existing-item1"]); - - Assert.Equal(666, options.Dictionary["existing-item1"]); - Assert.Equal(2, options.Dictionary["existing-item2"]); - Assert.Equal(3, options.Dictionary["item3"]); - } - - [Fact] - public void BindNonInstantiatedIReadOnlyDictionary() - { - var dic = new Dictionary - { - {"Dictionary:item1", "1"}, - {"Dictionary:item2", "2"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.Dictionary.Count); - - Assert.Equal(1, options.Dictionary["item1"]); - Assert.Equal(2, options.Dictionary["item2"]); - } - - [Fact] - public void BindInstantiatedConcreteDictionary_OverwritesOriginal() - { - var dic = new Dictionary - { - {"Dictionary:existing-item1", "666"}, - {"Dictionary:item3", "3"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(3, options.Dictionary.Count); - - // overwrites original - Assert.Equal(666, ConfigWithInstantiatedConcreteDictionary._existingDictionary["existing-item1"]); - Assert.Equal(666, options.Dictionary["existing-item1"]); - Assert.Equal(2, options.Dictionary["existing-item2"]); - Assert.Equal(3, options.Dictionary["item3"]); - } - - [Fact] - public void CanBindInstantiatedReadOnlyDictionary() - { - var dic = new Dictionary - { - {"InstantiatedReadOnlyDictionaryWithWithSomeValues:item3", "3"}, - {"InstantiatedReadOnlyDictionaryWithWithSomeValues:item4", "4"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - var resultingDictionary = options.InstantiatedReadOnlyDictionaryWithWithSomeValues; - - Assert.Equal(4, resultingDictionary.Count); - Assert.Equal(1, resultingDictionary["existing-item1"]); - Assert.Equal(2, resultingDictionary["existing-item2"]); - Assert.Equal(3, resultingDictionary["item3"]); - Assert.Equal(4, resultingDictionary["item4"]); - } - - [Fact] - public void CanBindNonInstantiatedReadOnlyDictionary() - { - var dic = new Dictionary - { - {"NonInstantiatedReadOnlyDictionary:item3", "3"}, - {"NonInstantiatedReadOnlyDictionary:item4", "4"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.NonInstantiatedReadOnlyDictionary.Count); - Assert.Equal(3, options.NonInstantiatedReadOnlyDictionary["item3"]); - Assert.Equal(4, options.NonInstantiatedReadOnlyDictionary["item4"]); - } - - - [Fact] - public void CanBindNonInstantiatedDictionaryOfISet() - { - var dic = new Dictionary - { - {"NonInstantiatedDictionaryWithISet:foo:0", "foo-1"}, - {"NonInstantiatedDictionaryWithISet:foo:1", "foo-2"}, - {"NonInstantiatedDictionaryWithISet:bar:0", "bar-1"}, - {"NonInstantiatedDictionaryWithISet:bar:1", "bar-2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.NonInstantiatedDictionaryWithISet.Count); - Assert.Equal("foo-1", options.NonInstantiatedDictionaryWithISet["foo"].ElementAt(0)); - Assert.Equal("foo-2", options.NonInstantiatedDictionaryWithISet["foo"].ElementAt(1)); - Assert.Equal("bar-1", options.NonInstantiatedDictionaryWithISet["bar"].ElementAt(0)); - Assert.Equal("bar-2", options.NonInstantiatedDictionaryWithISet["bar"].ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedDictionaryOfISet() - { - var dic = new Dictionary - { - {"InstantiatedDictionaryWithHashSet:foo:0", "foo-1"}, - {"InstantiatedDictionaryWithHashSet:foo:1", "foo-2"}, - {"InstantiatedDictionaryWithHashSet:bar:0", "bar-1"}, - {"InstantiatedDictionaryWithHashSet:bar:1", "bar-2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.InstantiatedDictionaryWithHashSet.Count); - Assert.Equal("foo-1", options.InstantiatedDictionaryWithHashSet["foo"].ElementAt(0)); - Assert.Equal("foo-2", options.InstantiatedDictionaryWithHashSet["foo"].ElementAt(1)); - Assert.Equal("bar-1", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(0)); - Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues() - { - var dic = new Dictionary - { - {"InstantiatedDictionaryWithHashSetWithSomeValues:foo:0", "foo-1"}, - {"InstantiatedDictionaryWithHashSetWithSomeValues:foo:1", "foo-2"}, - {"InstantiatedDictionaryWithHashSetWithSomeValues:bar:0", "bar-1"}, - {"InstantiatedDictionaryWithHashSetWithSomeValues:bar:1", "bar-2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(3, options.InstantiatedDictionaryWithHashSetWithSomeValues.Count); - Assert.Equal("existing1", options.InstantiatedDictionaryWithHashSetWithSomeValues["item1"].ElementAt(0)); - Assert.Equal("existing2", options.InstantiatedDictionaryWithHashSetWithSomeValues["item1"].ElementAt(1)); - - Assert.Equal("foo-1", options.InstantiatedDictionaryWithHashSetWithSomeValues["foo"].ElementAt(0)); - Assert.Equal("foo-2", options.InstantiatedDictionaryWithHashSetWithSomeValues["foo"].ElementAt(1)); - Assert.Equal("bar-1", options.InstantiatedDictionaryWithHashSetWithSomeValues["bar"].ElementAt(0)); - Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSetWithSomeValues["bar"].ElementAt(1)); - } - - [Fact] - public void ThrowsForCustomIEnumerableCollection() - { - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - ["CustomIEnumerableCollection:0"] = "Yo!", - }); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Get()); - Assert.Equal( - SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomCollectionDerivedFromIEnumerableT)), - exception.Message); - } - - [Fact] - public void ThrowsForCustomICollection() - { - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - ["CustomCollection:0"] = "Yo!", - }); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Get()); - Assert.Equal( - SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomCollectionDerivedFromICollectionT)), - exception.Message); - } - - [Fact] - public void ThrowsForCustomDictionary() - { - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - ["CustomDictionary:0"] = "Yo!", - }); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Get()); - Assert.Equal( - SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomDictionary)), - exception.Message); - } - - [Fact] - public void ThrowsForCustomSet() - { - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - ["CustomSet:0"] = "Yo!", - }); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Get()); - Assert.Equal( - SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomSet)), - exception.Message); - } - - [Fact] - public void CanBindInstantiatedISet() - { - var dic = new Dictionary - { - {"InstantiatedISet:0", "Yo1"}, - {"InstantiatedISet:1", "Yo2"}, - {"InstantiatedISet:2", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.InstantiatedISet.Count()); - Assert.Equal("Yo1", options.InstantiatedISet.ElementAt(0)); - Assert.Equal("Yo2", options.InstantiatedISet.ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedISetWithSomeValues() - { - var dic = new Dictionary - { - {"InstantiatedISetWithSomeValues:0", "Yo1"}, - {"InstantiatedISetWithSomeValues:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(4, options.InstantiatedISetWithSomeValues.Count); - Assert.Equal("existing1", options.InstantiatedISetWithSomeValues.ElementAt(0)); - Assert.Equal("existing2", options.InstantiatedISetWithSomeValues.ElementAt(1)); - Assert.Equal("Yo1", options.InstantiatedISetWithSomeValues.ElementAt(2)); - Assert.Equal("Yo2", options.InstantiatedISetWithSomeValues.ElementAt(3)); - } - - [Fact] - public void CanBindInstantiatedHashSetWithSomeValues() - { - var dic = new Dictionary - { - {"InstantiatedHashSetWithSomeValues:0", "Yo1"}, - {"InstantiatedHashSetWithSomeValues:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(4, options.InstantiatedHashSetWithSomeValues.Count); - Assert.Equal("existing1", options.InstantiatedHashSetWithSomeValues.ElementAt(0)); - Assert.Equal("existing2", options.InstantiatedHashSetWithSomeValues.ElementAt(1)); - Assert.Equal("Yo1", options.InstantiatedHashSetWithSomeValues.ElementAt(2)); - Assert.Equal("Yo2", options.InstantiatedHashSetWithSomeValues.ElementAt(3)); - } - - [Fact] - public void CanBindNonInstantiatedHashSet() - { - var dic = new Dictionary - { - {"NonInstantiatedHashSet:0", "Yo1"}, - {"NonInstantiatedHashSet:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.NonInstantiatedHashSet.Count); - Assert.Equal("Yo1", options.NonInstantiatedHashSet.ElementAt(0)); - Assert.Equal("Yo2", options.NonInstantiatedHashSet.ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedSortedSetWithSomeValues() - { - var dic = new Dictionary - { - {"InstantiatedSortedSetWithSomeValues:0", "Yo1"}, - {"InstantiatedSortedSetWithSomeValues:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(4, options.InstantiatedSortedSetWithSomeValues.Count); - Assert.Equal("existing1", options.InstantiatedSortedSetWithSomeValues.ElementAt(0)); - Assert.Equal("existing2", options.InstantiatedSortedSetWithSomeValues.ElementAt(1)); - Assert.Equal("Yo1", options.InstantiatedSortedSetWithSomeValues.ElementAt(2)); - Assert.Equal("Yo2", options.InstantiatedSortedSetWithSomeValues.ElementAt(3)); - } - - [Fact] - public void CanBindNonInstantiatedSortedSetWithSomeValues() - { - var dic = new Dictionary - { - {"NonInstantiatedSortedSetWithSomeValues:0", "Yo1"}, - {"NonInstantiatedSortedSetWithSomeValues:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.NonInstantiatedSortedSetWithSomeValues.Count); - Assert.Equal("Yo1", options.NonInstantiatedSortedSetWithSomeValues.ElementAt(0)); - Assert.Equal("Yo2", options.NonInstantiatedSortedSetWithSomeValues.ElementAt(1)); - } - - [Fact] - public void DoesNotBindInstantiatedISetWithUnsupportedKeys() - { - var dic = new Dictionary - { - {"HashSetWithUnsupportedKey:0", "Yo1"}, - {"HashSetWithUnsupportedKey:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(0, options.HashSetWithUnsupportedKey.Count); - } - - [Fact] - public void DoesNotBindUninstantiatedISetWithUnsupportedKeys() - { - var dic = new Dictionary - { - {"UninstantiatedHashSetWithUnsupportedKey:0", "Yo1"}, - {"UninstantiatedHashSetWithUnsupportedKey:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Null(options.UninstantiatedHashSetWithUnsupportedKey); - } - - [Fact] - public void CanBindInstantiatedIEnumerableWithItems() - { - var dic = new Dictionary - { - {"InstantiatedIEnumerable:0", "Yo1"}, - {"InstantiatedIEnumerable:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.InstantiatedIEnumerable.Count()); - Assert.Equal("Yo1", options.InstantiatedIEnumerable.ElementAt(0)); - Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedCustomICollectionWithoutAnAddMethodWithItems() - { - var dic = new Dictionary - { - {"InstantiatedCustomICollectionWithoutAnAddMethod:0", "Yo1"}, - {"InstantiatedCustomICollectionWithoutAnAddMethod:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.InstantiatedCustomICollectionWithoutAnAddMethod.Count); - Assert.Equal("Yo1", options.InstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(0)); - Assert.Equal("Yo2", options.InstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1)); - } - - [Fact] - public void CanBindNonInstantiatedCustomICollectionWithoutAnAddMethodWithItems() - { - var dic = new Dictionary - { - {"NonInstantiatedCustomICollectionWithoutAnAddMethod:0", "Yo1"}, - {"NonInstantiatedCustomICollectionWithoutAnAddMethod:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.NonInstantiatedCustomICollectionWithoutAnAddMethod.Count); - Assert.Equal("Yo1", options.NonInstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(0)); - Assert.Equal("Yo2", options.NonInstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedICollectionWithItems() - { - var dic = new Dictionary - { - {"InstantiatedICollection:0", "Yo1"}, - {"InstantiatedICollection:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.InstantiatedICollection.Count()); - Assert.Equal("Yo1", options.InstantiatedICollection.ElementAt(0)); - Assert.Equal("Yo2", options.InstantiatedICollection.ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedIReadOnlyCollectionWithItems() - { - var dic = new Dictionary - { - {"InstantiatedIReadOnlyCollection:0", "Yo1"}, - {"InstantiatedIReadOnlyCollection:1", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.InstantiatedIReadOnlyCollection.Count); - Assert.Equal("Yo1", options.InstantiatedIReadOnlyCollection.ElementAt(0)); - Assert.Equal("Yo2", options.InstantiatedIReadOnlyCollection.ElementAt(1)); - } - - [Fact] - public void CanBindInstantiatedIEnumerableWithNullItems() - { - var dic = new Dictionary - { - {"InstantiatedIEnumerable:0", null}, - {"InstantiatedIEnumerable:1", "Yo1"}, - {"InstantiatedIEnumerable:2", "Yo2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - - var config = configurationBuilder.Build(); - - var options = config.Get()!; - - Assert.Equal(2, options.InstantiatedIEnumerable.Count()); - Assert.Equal("Yo1", options.InstantiatedIEnumerable.ElementAt(0)); - Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1)); - } - - [Fact] - public void EmptyStringIsNullable() - { - var dic = new Dictionary - { - {"empty", ""}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - Assert.Null(config.GetValue("empty")); - Assert.Null(config.GetValue("empty")); - } - - [Fact] - public void GetScalarNullable() - { - 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")); - } - - [Fact] - public void CanBindToObjectProperty() - { - var dic = new Dictionary - { - {"Object", "whatever" } - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = new ComplexOptions(); - config.Bind(options); - - Assert.Equal("whatever", options.Object); - } - - [Fact] - public void GetNullValue() - { - var dic = new Dictionary - { - {"Integer", null}, - {"Boolean", null}, - {"Nested:Integer", null}, - {"Object", null } - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - Assert.False(config.GetValue("Boolean")); - Assert.Equal(0, config.GetValue("Integer")); - Assert.Equal(0, config.GetValue("Nested:Integer")); - Assert.Null(config.GetValue("Object")); - Assert.False(config.GetSection("Boolean").Get()); - Assert.Equal(0, config.GetSection("Integer").Get()); - Assert.Equal(0, config.GetSection("Nested:Integer").Get()); - Assert.Null(config.GetSection("Object").Get()); - } - - [Fact] - public void ThrowsIfPropertyInConfigMissingInModel() - { - var dic = new Dictionary - { - {"ThisDoesNotExistInTheModel", "42"}, - {"Integer", "-2"}, - {"Boolean", "TRUe"}, - {"Nested:Integer", "11"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var instance = new ComplexOptions(); - - var ex = Assert.Throws( - () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); - - string expectedMessage = SR.Format(SR.Error_MissingConfig, - nameof(BinderOptions.ErrorOnUnknownConfiguration), nameof(BinderOptions), typeof(ComplexOptions), "'ThisDoesNotExistInTheModel'"); - - Assert.Equal(expectedMessage, ex.Message); - } - [Fact] - public void ThrowsIfPropertyInConfigMissingInNestedModel() - { - var dic = new Dictionary - { - {"Nested:ThisDoesNotExistInTheModel", "42"}, - {"Integer", "-2"}, - {"Boolean", "TRUe"}, - {"Nested:Integer", "11"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var instance = new ComplexOptions(); - - string expectedMessage = SR.Format(SR.Error_MissingConfig, - nameof(BinderOptions.ErrorOnUnknownConfiguration), nameof(BinderOptions), typeof(NestedOptions), "'ThisDoesNotExistInTheModel'"); - - var ex = Assert.Throws( - () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); - - Assert.Equal(expectedMessage, ex.Message); - } - - [Fact] - public void GetDefaultsWhenDataDoesNotExist() - { - var dic = new Dictionary - { - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - Assert.False(config.GetValue("Boolean")); - Assert.Equal(0, config.GetValue("Integer")); - Assert.Equal(0, config.GetValue("Nested:Integer")); - Assert.Null(config.GetValue("Object")); - Assert.True(config.GetValue("Boolean", true)); - Assert.Equal(3, config.GetValue("Integer", 3)); - Assert.Equal(1, config.GetValue("Nested:Integer", 1)); - var foo = new ComplexOptions(); - Assert.Same(config.GetValue("Object", foo), foo); - } - - [Fact] - public void GetUri() - { - var dic = new Dictionary - { - {"AnUri", "http://www.bing.com"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var uri = config.GetValue("AnUri"); - - Assert.Equal("http://www.bing.com", uri.OriginalString); - } - - [Theory] - [InlineData("2147483647", typeof(int))] - [InlineData("4294967295", typeof(uint))] - [InlineData("32767", typeof(short))] - [InlineData("65535", typeof(ushort))] - [InlineData("-9223372036854775808", typeof(long))] - [InlineData("18446744073709551615", typeof(ulong))] - [InlineData("trUE", typeof(bool))] - [InlineData("255", typeof(byte))] - [InlineData("127", typeof(sbyte))] - [InlineData("\uffff", typeof(char))] - [InlineData("79228162514264337593543950335", typeof(decimal))] - [InlineData("1.79769e+308", typeof(double))] - [InlineData("3.40282347E+38", typeof(float))] - [InlineData("2015-12-24T07:34:42-5:00", typeof(DateTime))] - [InlineData("12/24/2015 13:44:55 +4", typeof(DateTimeOffset))] - [InlineData("99.22:22:22.1234567", typeof(TimeSpan))] - [InlineData("http://www.bing.com", typeof(Uri))] - // enum test - [InlineData("Constructor", typeof(AttributeTargets))] - [InlineData("CA761232-ED42-11CE-BACD-00AA0057B223", typeof(Guid))] - public void CanReadAllSupportedTypes(string value, Type type) - { - // arrange - var dic = new Dictionary - { - {"Value", value} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var optionsType = typeof(GenericOptions<>).MakeGenericType(type); - var options = Activator.CreateInstance(optionsType); - var expectedValue = TypeDescriptor.GetConverter(type).ConvertFromInvariantString(value); - - // act - config.Bind(options); - var optionsValue = options.GetType().GetProperty("Value").GetValue(options); - var getValueValue = config.GetValue(type, "Value"); - var getValue = config.GetSection("Value").Get(type); - - // assert - Assert.Equal(expectedValue, optionsValue); - Assert.Equal(expectedValue, getValue); - Assert.Equal(expectedValue, getValueValue); - } - - [Theory] - [InlineData(typeof(int))] - [InlineData(typeof(uint))] - [InlineData(typeof(short))] - [InlineData(typeof(ushort))] - [InlineData(typeof(long))] - [InlineData(typeof(ulong))] - [InlineData(typeof(bool))] - [InlineData(typeof(byte))] - [InlineData(typeof(sbyte))] - [InlineData(typeof(char))] - [InlineData(typeof(decimal))] - [InlineData(typeof(double))] - [InlineData(typeof(float))] - [InlineData(typeof(DateTime))] - [InlineData(typeof(DateTimeOffset))] - [InlineData(typeof(TimeSpan))] - [InlineData(typeof(AttributeTargets))] - [InlineData(typeof(Guid))] - public void ConsistentExceptionOnFailedBinding(Type type) - { - // arrange - const string IncorrectValue = "Invalid data"; - const string ConfigKey = "Value"; - var dic = new Dictionary - { - {ConfigKey, IncorrectValue} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var optionsType = typeof(GenericOptions<>).MakeGenericType(type); - var options = Activator.CreateInstance(optionsType); - - // act - var exception = Assert.Throws( - () => config.Bind(options)); - - var getValueException = Assert.Throws( - () => config.GetValue(type, "Value")); - - var getException = Assert.Throws( - () => config.GetSection("Value").Get(type)); - - // assert - Assert.NotNull(exception.InnerException); - Assert.NotNull(getException.InnerException); - Assert.Equal( - SR.Format(SR.Error_FailedBinding, ConfigKey, type), - exception.Message); - Assert.Equal( - SR.Format(SR.Error_FailedBinding, ConfigKey, type), - getException.Message); - Assert.Equal( - SR.Format(SR.Error_FailedBinding, ConfigKey, type), - getValueException.Message); - } - - [Fact] - public void ExceptionOnFailedBindingIncludesPath() - { - const string IncorrectValue = "Invalid data"; - const string ConfigKey = "Nested:Value"; - - var dic = new Dictionary - { - {ConfigKey, IncorrectValue} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = new OptionsWithNesting(); - - var exception = Assert.Throws( - () => config.Bind(options)); - - Assert.Equal(SR.Format(SR.Error_FailedBinding, ConfigKey, typeof(int)), - exception.Message); - } - - [Fact] - public void BinderIgnoresIndexerProperties() - { - var configurationBuilder = new ConfigurationBuilder(); - var config = configurationBuilder.Build(); - config.Bind(new List()); - } - - [Fact] - public void BindCanReadComplexProperties() - { - var dic = new Dictionary - { - {"Integer", "-2"}, - {"Boolean", "TRUe"}, - {"Nested:Integer", "11"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var instance = new ComplexOptions(); - config.Bind(instance); - - Assert.True(instance.Boolean); - Assert.Equal(-2, instance.Integer); - Assert.Equal(11, instance.Nested.Integer); - } - - [Fact] - public void GetCanReadComplexProperties() - { - var dic = new Dictionary - { - {"Integer", "-2"}, - {"Boolean", "TRUe"}, - {"Nested:Integer", "11"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = new ComplexOptions(); - config.Bind(options); - - Assert.True(options.Boolean); - Assert.Equal(-2, options.Integer); - Assert.Equal(11, options.Nested.Integer); - } - - [Fact] - public void BindCanReadInheritedProperties() - { - var dic = new Dictionary - { - {"Integer", "-2"}, - {"Boolean", "TRUe"}, - {"Nested:Integer", "11"}, - {"Virtual", "Sup"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var instance = new DerivedOptions(); - config.Bind(instance); - - Assert.True(instance.Boolean); - Assert.Equal(-2, instance.Integer); - Assert.Equal(11, instance.Nested.Integer); - Assert.Equal("Derived:Sup", instance.Virtual); - } - - [Fact] - public void GetCanReadInheritedProperties() - { - var dic = new Dictionary - { - {"Integer", "-2"}, - {"Boolean", "TRUe"}, - {"Nested:Integer", "11"}, - {"Virtual", "Sup"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = new DerivedOptions(); - config.Bind(options); - - Assert.True(options.Boolean); - Assert.Equal(-2, options.Integer); - Assert.Equal(11, options.Nested.Integer); - Assert.Equal("Derived:Sup", options.Virtual); - } - - [Fact] - public void GetCanReadStaticProperty() - { - var dic = new Dictionary - { - {"StaticProperty", "stuff"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - var options = new ComplexOptions(); - config.Bind(options); - - Assert.Equal("stuff", ComplexOptions.StaticProperty); - } - - [Fact] - public void BindCanReadStaticProperty() - { - var dic = new Dictionary - { - {"StaticProperty", "other stuff"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var instance = new ComplexOptions(); - config.Bind(instance); - - Assert.Equal("other stuff", ComplexOptions.StaticProperty); - } - - [Fact] - public void CanGetComplexOptionsWhichHasAlsoHasValue() - { - var dic = new Dictionary - { - {"obj", "whut" }, - {"obj:Integer", "-2"}, - {"obj:Boolean", "TRUe"}, - {"obj:Nested:Integer", "11"} - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.GetSection("obj").Get(); - Assert.NotNull(options); - Assert.True(options.Boolean); - Assert.Equal(-2, options.Integer); - Assert.Equal(11, options.Nested.Integer); - } - - [Theory] - [InlineData("ReadOnly")] - [InlineData("PrivateSetter")] - [InlineData("ProtectedSetter")] - [InlineData("InternalSetter")] - [InlineData("InternalProperty")] - [InlineData("PrivateProperty")] - [InlineData("ProtectedProperty")] - [InlineData("ProtectedPrivateSet")] - public void GetIgnoresTests(string property) - { - var dic = new Dictionary - { - {property, "stuff"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Null(options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); - } - - [Theory] - [InlineData("PrivateSetter")] - [InlineData("ProtectedSetter")] - [InlineData("InternalSetter")] - [InlineData("InternalProperty")] - [InlineData("PrivateProperty")] - [InlineData("ProtectedProperty")] - [InlineData("ProtectedPrivateSet")] - public void GetCanSetNonPublicWhenSet(string property) - { - var dic = new Dictionary - { - {property, "stuff"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(o => o.BindNonPublicProperties = true); - Assert.Equal("stuff", options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); - } - - [Theory] - [InlineData("InternalReadOnly")] - [InlineData("PrivateReadOnly")] - [InlineData("ProtectedReadOnly")] - public void NonPublicModeGetStillIgnoresReadonly(string property) - { - var dic = new Dictionary - { - {property, "stuff"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(o => o.BindNonPublicProperties = true); - Assert.Null(options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); - } - - [Theory] - [InlineData("ReadOnly")] - [InlineData("PrivateSetter")] - [InlineData("ProtectedSetter")] - [InlineData("InternalSetter")] - [InlineData("InternalProperty")] - [InlineData("PrivateProperty")] - [InlineData("ProtectedProperty")] - [InlineData("ProtectedPrivateSet")] - public void BindIgnoresTests(string property) - { - var dic = new Dictionary - { - {property, "stuff"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = new ComplexOptions(); - config.Bind(options); - - Assert.Null(options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); - } - - [Theory] - [InlineData("PrivateSetter")] - [InlineData("ProtectedSetter")] - [InlineData("InternalSetter")] - [InlineData("InternalProperty")] - [InlineData("PrivateProperty")] - [InlineData("ProtectedProperty")] - [InlineData("ProtectedPrivateSet")] - public void BindCanSetNonPublicWhenSet(string property) - { - var dic = new Dictionary - { - {property, "stuff"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = new ComplexOptions(); - config.Bind(options, o => o.BindNonPublicProperties = true); - Assert.Equal("stuff", options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); - } - - [Theory] - [InlineData("InternalReadOnly")] - [InlineData("PrivateReadOnly")] - [InlineData("ProtectedReadOnly")] - public void NonPublicModeBindStillIgnoresReadonly(string property) - { - var dic = new Dictionary - { - {property, "stuff"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = new ComplexOptions(); - config.Bind(options, o => o.BindNonPublicProperties = true); - Assert.Null(options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); - } - - [Fact] - public void ExceptionWhenTryingToBindToInterface() - { - var input = new Dictionary - { - {"ISomeInterfaceProperty:Subkey", "x"} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Bind(new TestOptions())); - Assert.Equal( - SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ISomeInterface)), - exception.Message); - } - - [Fact] - public void ExceptionWhenTryingToBindClassWithoutParameterlessConstructor() - { - var input = new Dictionary - { - {"ClassWithoutPublicConstructorProperty:Subkey", "x"} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Bind(new TestOptions())); - Assert.Equal( - SR.Format(SR.Error_MissingPublicInstanceConstructor, typeof(ClassWithoutPublicConstructor)), - exception.Message); - } - - [Fact] - public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParameters() - { - var input = new Dictionary - { - {"ClassWhereParametersDoNotMatchPropertiesProperty:Name", "John"}, - {"ClassWhereParametersDoNotMatchPropertiesProperty:Address", "123, Abc St."}, - {"ClassWhereParametersDoNotMatchPropertiesProperty:Age", "42"} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Bind(new TestOptions())); - Assert.Equal( - SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, typeof(ClassWhereParametersDoNotMatchProperties), "age"), - exception.Message); - } - - [Fact] - public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() - { - var input = new Dictionary - { - {"LineProperty:Color", "Red"}, - {"LineProperty:Length", "22"} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Bind(new TestOptions())); - Assert.Equal( - SR.Format(SR.Error_ParameterHasNoMatchingConfig, typeof(Line), nameof(Line.Thickness)), - exception.Message); - } - - [Fact] - public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor() - { - var input = new Dictionary - { - {"ClassWhereParametersDoNotMatchPropertiesProperty:Name", "John"}, - {"ClassWhereParametersDoNotMatchPropertiesProperty:Address", "123, Abc St."}, - {"ClassWhereParametersDoNotMatchPropertiesProperty:Age", "42"} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Bind(new TestOptions())); - Assert.Equal( - SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, typeof(ClassWhereParametersDoNotMatchProperties), "age"), - exception.Message); - } - - [Fact] - public void BindsToClassConstructorParametersWithDefaultValues() - { - var input = new Dictionary - { - {"ClassWhereParametersHaveDefaultValueProperty:Name", "John"}, - {"ClassWhereParametersHaveDefaultValueProperty:Address", "123, Abc St."} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - TestOptions testOptions = new TestOptions(); - - config.Bind(testOptions); - Assert.Equal("John", testOptions.ClassWhereParametersHaveDefaultValueProperty.Name); - Assert.Equal("123, Abc St.", testOptions.ClassWhereParametersHaveDefaultValueProperty.Address); - Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.Age); - } - - [Fact] - public void FieldsNotSupported_ExceptionBindingToConstructorWithParameterMatchingAField() - { - var input = new Dictionary - { - {"ClassWhereParametersMatchPropertiesAndFieldsProperty:Name", "John"}, - {"ClassWhereParametersMatchPropertiesAndFieldsProperty:Address", "123, Abc St."}, - {"ClassWhereParametersMatchPropertiesAndFieldsProperty:Age", "42"} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Bind(new TestOptions())); - - Assert.Equal( - SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, typeof(ClassWhereParametersMatchPropertiesAndFields), "age"), - exception.Message); - } - - [Fact] - public void BindsToRecordPrimaryConstructorParametersWithDefaultValues() - { - var input = new Dictionary - { - {"RecordWhereParametersHaveDefaultValueProperty:Name", "John"}, - {"RecordWhereParametersHaveDefaultValueProperty:Address", "123, Abc St."} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - TestOptions testOptions = new TestOptions(); - - config.Bind(testOptions); - Assert.Equal("John", testOptions.RecordWhereParametersHaveDefaultValueProperty.Name); - Assert.Equal("123, Abc St.", testOptions.RecordWhereParametersHaveDefaultValueProperty.Address); - Assert.Equal(42, testOptions.RecordWhereParametersHaveDefaultValueProperty.Age); - } - - [Fact] - public void ExceptionWhenTryingToBindToTypeThrowsWhenActivated() - { - var input = new Dictionary - { - {"ThrowsWhenActivatedProperty:subkey", "x"} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Bind(new TestOptions())); - Assert.NotNull(exception.InnerException); - Assert.Equal( - SR.Format(SR.Error_FailedToActivate, typeof(ThrowsWhenActivated)), - exception.Message); - } - - [Fact] - public void ExceptionIncludesKeyOfFailedBinding() - { - var input = new Dictionary - { - {"NestedOptionsProperty:NestedOptions2Property:ISomeInterfaceProperty:subkey", "x"} - }; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(input); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Bind(new TestOptions())); - Assert.Equal( - SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ISomeInterface)), - exception.Message); - } - - [Fact] - public void CanBindValueTypeOptions() - { - var dic = new Dictionary - { - {"MyInt32", "42"}, - {"MyString", "hello world"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(42, options.MyInt32); - Assert.Equal("hello world", options.MyString); - } - - [Fact] - public void CanBindImmutableClass() - { - var dic = new Dictionary - { - {"Length", "42"}, - {"Color", "Green"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(42, options.Length); - Assert.Equal("Green", options.Color); - } - - [Fact] - public void CanBindMutableClassWitNestedImmutableObject() - { - var dic = new Dictionary - { - {"ContainerName", "Container123"}, - {"LengthAndColor:Length", "42"}, - {"LengthAndColor:Color", "Green"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal("Container123", options.ContainerName); - Assert.Equal(42, options.LengthAndColor.Length); - Assert.Equal("Green", options.LengthAndColor.Color); - } - - // If the immutable type has multiple public parameterized constructors, then throw - // an exception. - [Fact] - public void CanBindImmutableClass_ThrowsOnMultipleParameterizedConstructors() - { - var dic = new Dictionary - { - {"String1", "s1"}, - {"Int1", "1"}, - {"String2", "s2"}, - {"Int2", "2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - string expectedMessage = SR.Format(SR.Error_MultipleParameterizedConstructors, "Microsoft.Extensions.Configuration.Binder.Test.ConfigurationBinderTests+ImmutableClassWithMultipleParameterizedConstructors"); - - var ex = Assert.Throws(() => config.Get()); - - Assert.Equal(expectedMessage, ex.Message); - } - - // If the immutable type has a parameterized constructor, then throw - // that constructor has an 'in' parameter - [Fact] - public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnInParameter() - { - var dic = new Dictionary - { - {"String1", "s1"}, - {"Int1", "1"}, - {"String2", "s2"}, - {"Int2", "2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Test.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithInParameter", "string1"); - - var ex = Assert.Throws(() => config.Get()); - - Assert.Equal(expectedMessage, ex.Message); - } - - // If the immutable type has a parameterized constructors, then throw - // that constructor has a 'ref' parameter - [Fact] - public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithARefParameter() - { - var dic = new Dictionary - { - {"String1", "s1"}, - {"Int1", "1"}, - {"String2", "s2"}, - {"Int2", "2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Test.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithRefParameter", "int1"); - - var ex = Assert.Throws(() => config.Get()); - - Assert.Equal(expectedMessage, ex.Message); - } - - // If the immutable type has a parameterized constructors, then throw - // if the constructor has an 'out' parameter - [Fact] - public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnOutParameter() - { - var dic = new Dictionary - { - {"String1", "s1"}, - {"Int1", "1"}, - {"String2", "s2"}, - {"Int2", "2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Test.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithOutParameter", "int2"); - - var ex = Assert.Throws(() => config.Get()); - - Assert.Equal(expectedMessage, ex.Message); - } - - [Fact] - public void CanBindMutableStruct_UnmatchedConstructorsAreIgnored() - { - var dic = new Dictionary - { - {"Length", "42"}, - {"Color", "Green"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(42, options.Length); - Assert.Equal("Green", options.Color); - } - - // If the immutable type has a public parameterized constructor, - // then pick it. - [Fact] - public void CanBindImmutableClass_PicksParameterizedConstructorIfNoParameterlessConstructorExists() - { - var dic = new Dictionary - { - {"String1", "s1"}, - {"Int1", "1"}, - {"String2", "s2"}, - {"Int2", "2"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal("s1", options.String1); - Assert.Equal("s2", options.String2); - Assert.Equal(1, options.Int1); - Assert.Equal(2, options.Int2); - } - - [Fact] - public void CanBindSemiImmutableClass() - { - var dic = new Dictionary - { - {"Length", "42"}, - {"Color", "Green"}, - {"Thickness", "1.23"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(42, options.Length); - Assert.Equal("Green", options.Color); - Assert.Equal(1.23m, options.Thickness); - } - - [Fact] - public void CanBindSemiImmutableClass_WithInitProperties() - { - var dic = new Dictionary - { - {"Length", "42"}, - {"Color", "Green"}, - {"Thickness", "1.23"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(42, options.Length); - Assert.Equal("Green", options.Color); - Assert.Equal(1.23m, options.Thickness); - } - - [Fact] - public void CanBindRecordOptions() - { - var dic = new Dictionary - { - {"Length", "42"}, - {"Color", "Green"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(42, options.Length); - Assert.Equal("Green", options.Color); - } - - [Fact] - public void CanBindRecordStructOptions() - { - var dic = new Dictionary - { - {"Length", "42"}, - {"Color", "Green"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(42, options.Length); - Assert.Equal("Green", options.Color); - } - - [Fact] - public void CanBindNestedRecordOptions() - { - var dic = new Dictionary - { - {"Number", "1"}, - {"Nested1:ValueA", "Cool"}, - {"Nested1:ValueB", "42"}, - {"Nested2:ValueA", "Uncool"}, - {"Nested2:ValueB", "24"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(1, options.Number); - Assert.Equal("Cool", options.Nested1.ValueA); - Assert.Equal(42, options.Nested1.ValueB); - Assert.Equal("Uncool", options.Nested2.ValueA); - Assert.Equal(24, options.Nested2.ValueB); - } - - [Fact] - public void CanBindOnParametersAndProperties_PropertiesAreSetAfterTheConstructor() - { - var dic = new Dictionary - { - {"Length", "42"}, - {"Color", "Green"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(42, options.Length); - Assert.Equal("the color is Green", options.Color); - } - - [Fact] - public void CanBindReadonlyRecordStructOptions() - { - var dic = new Dictionary - { - {"Length", "42"}, - {"Color", "Green"}, - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(42, options.Length); - Assert.Equal("Green", options.Color); - } - - [Fact] - public void CanBindByteArray() - { - var bytes = new byte[] { 1, 2, 3, 4 }; - var dic = new Dictionary - { - { "MyByteArray", Convert.ToBase64String(bytes) } - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Equal(bytes, options.MyByteArray); - } - - [Fact] - public void CanBindByteArrayWhenValueIsNull() - { - var dic = new Dictionary - { - { "MyByteArray", null } - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var options = config.Get(); - Assert.Null(options.MyByteArray); - } - - [Fact] - public void ExceptionWhenTryingToBindToByteArray() - { - var dic = new Dictionary - { - { "MyByteArray", "(not a valid base64 string)" } - }; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(dic); - var config = configurationBuilder.Build(); - - var exception = Assert.Throws( - () => config.Get()); - Assert.Equal( - SR.Format(SR.Error_FailedBinding, "MyByteArray", typeof(byte[])), - exception.Message); - } - - [Fact] - public void DoesNotReadPropertiesUnnecessarily() - { - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - { nameof(ClassWithReadOnlyPropertyThatThrows.Safe), "value" }, - { nameof(ClassWithReadOnlyPropertyThatThrows.StringThrows), "value" }, - { $"{nameof(ClassWithReadOnlyPropertyThatThrows.EnumerableThrows)}:0", "0" }, - }); - IConfiguration config = configurationBuilder.Build(); - - ClassWithReadOnlyPropertyThatThrows bound = config.Get(); - Assert.Equal("value", bound.Safe); - } - - /// - /// Binding to mutable structs is important to support properties - /// like JsonConsoleFormatterOptions.JsonWriterOptions. - /// - [Fact] - public void CanBindNestedStructProperties() - { - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - { "ReadWriteNestedStruct:String", "s" }, - { "ReadWriteNestedStruct:DeeplyNested:Int32", "100" }, - { "ReadWriteNestedStruct:DeeplyNested:Boolean", "true" }, - }); - IConfiguration config = configurationBuilder.Build(); - - StructWithNestedStructs bound = config.Get(); - Assert.Equal("s", bound.ReadWriteNestedStruct.String); - Assert.Equal(100, bound.ReadWriteNestedStruct.DeeplyNested.Int32); - Assert.True(bound.ReadWriteNestedStruct.DeeplyNested.Boolean); - } - - [Fact] - public void IgnoresReadOnlyNestedStructProperties() - { - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - { "ReadOnlyNestedStruct:String", "s" }, - { "ReadOnlyNestedStruct:DeeplyNested:Int32", "100" }, - { "ReadOnlyNestedStruct:DeeplyNested:Boolean", "true" }, - }); - IConfiguration config = configurationBuilder.Build(); - - StructWithNestedStructs bound = config.Get(); - Assert.Null(bound.ReadOnlyNestedStruct.String); - Assert.Equal(0, bound.ReadWriteNestedStruct.DeeplyNested.Int32); - Assert.False(bound.ReadWriteNestedStruct.DeeplyNested.Boolean); - } - - [Fact] - public void CanBindNullableNestedStructProperties() - { - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - { "NullableNestedStruct:String", "s" }, - { "NullableNestedStruct:DeeplyNested:Int32", "100" }, - { "NullableNestedStruct:DeeplyNested:Boolean", "true" }, - }); - IConfiguration config = configurationBuilder.Build(); - - StructWithNestedStructs bound = config.Get(); - Assert.NotNull(bound.NullableNestedStruct); - Assert.Equal("s", bound.NullableNestedStruct.Value.String); - Assert.Equal(100, bound.NullableNestedStruct.Value.DeeplyNested.Int32); - Assert.True(bound.NullableNestedStruct.Value.DeeplyNested.Boolean); - } - - [Fact] - public void CanBindVirtualProperties() - { - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - { $"{nameof(BaseClassWithVirtualProperty.Test)}:0", "1" }, - { $"{nameof(BaseClassWithVirtualProperty.TestGetSetOverridden)}", "2" }, - { $"{nameof(BaseClassWithVirtualProperty.TestGetOverridden)}", "3" }, - { $"{nameof(BaseClassWithVirtualProperty.TestSetOverridden)}", "4" }, - { $"{nameof(BaseClassWithVirtualProperty.TestNoOverridden)}", "5" }, - { $"{nameof(BaseClassWithVirtualProperty.TestVirtualSet)}", "6" } - }); - IConfiguration config = configurationBuilder.Build(); - - var test = new ClassOverridingVirtualProperty(); - config.Bind(test); - - Assert.Equal("1", Assert.Single(test.Test)); - Assert.Equal("2", test.TestGetSetOverridden); - Assert.Equal("3", test.TestGetOverridden); - Assert.Equal("4", test.TestSetOverridden); - Assert.Equal("5", test.TestNoOverridden); - Assert.Null(test.ExposeTestVirtualSet()); - } - - [Fact] - public void CanBindPrivatePropertiesFromBaseClass() - { - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddInMemoryCollection(new Dictionary - { - { "PrivateProperty", "a" } - }); - IConfiguration config = configurationBuilder.Build(); - - var test = new ClassOverridingVirtualProperty(); - config.Bind(test, b => b.BindNonPublicProperties = true); - Assert.Equal("a", test.ExposePrivatePropertyValue()); - } - - [Fact] - public void EnsureCallingThePropertySetter() - { - var json = @"{ - ""IPFiltering"": { - ""HttpStatusCode"": 401, - ""Blacklist"": [ ""192.168.0.10-192.168.10.20"", ""fe80::/10"" ] - } - }"; - - var configuration = new ConfigurationBuilder() - .AddJsonStream(TestStreamHelpers.StringToStream(json)) - .Build(); - - OptionWithCollectionProperties options = configuration.GetSection("IPFiltering").Get(); - - Assert.NotNull(options); - Assert.Equal(2, options.Blacklist.Count); - Assert.Equal("192.168.0.10-192.168.10.20", options.Blacklist.ElementAt(0)); - Assert.Equal("fe80::/10", options.Blacklist.ElementAt(1)); - - Assert.Equal(2, options.ParsedBlacklist.Count); // should be initialized when calling the options.Blacklist setter. - - Assert.Equal(401, options.HttpStatusCode); // exists in configuration and properly sets the property - Assert.Equal(2, options.OtherCode); // doesn't exist in configuration. the setter sets default value '2' - } - - public class OptionWithCollectionProperties - { - private int _otherCode; - private ICollection blacklist = new HashSet(); - - public ICollection Blacklist - { - get => this.blacklist; - set - { - this.blacklist = value ?? new HashSet(); - this.ParsedBlacklist = this.blacklist.Select(b => b).ToList(); - } - } - - public int HttpStatusCode { get; set; } = 0; - - // ParsedBlacklist initialized using the setter of Blacklist. - public ICollection ParsedBlacklist { get; private set; } = new HashSet(); - - // This property not having any match in the configuration. Still the setter need to be called during the binding. - public int OtherCode - { - get => _otherCode; - set => _otherCode = value == 0 ? 2 : value; - } - } - - private interface ISomeInterface - { - } - - private class ClassWithoutPublicConstructor - { - private ClassWithoutPublicConstructor() - { - } - } - - private class ThrowsWhenActivated - { - public ThrowsWhenActivated() - { - throw new Exception(); - } - } - - private class NestedOptions1 - { - public NestedOptions2 NestedOptions2Property { get; set; } - } - - private class NestedOptions2 - { - public ISomeInterface ISomeInterfaceProperty { get; set; } - } - - private class TestOptions - { - public ISomeInterface ISomeInterfaceProperty { get; set; } - - public ClassWithoutPublicConstructor ClassWithoutPublicConstructorProperty { get; set; } - public ClassWhereParametersDoNotMatchProperties ClassWhereParametersDoNotMatchPropertiesProperty { get; set; } - public Line LineProperty { get; set; } - public ClassWhereParametersHaveDefaultValue ClassWhereParametersHaveDefaultValueProperty { get; set; } - public ClassWhereParametersMatchPropertiesAndFields ClassWhereParametersMatchPropertiesAndFieldsProperty { get; set; } - public RecordWhereParametersHaveDefaultValue RecordWhereParametersHaveDefaultValueProperty { get; set; } - - public int IntProperty { get; set; } - - public ThrowsWhenActivated ThrowsWhenActivatedProperty { get; set; } - - public NestedOptions1 NestedOptionsProperty { get; set; } - } - - private class ClassWithReadOnlyPropertyThatThrows - { - public string StringThrows => throw new InvalidOperationException(nameof(StringThrows)); - - public IEnumerable EnumerableThrows => throw new InvalidOperationException(nameof(EnumerableThrows)); - - public string Safe { get; set; } - } - - private struct StructWithNestedStructs - { - public Nested ReadWriteNestedStruct { get; set; } - - public Nested ReadOnlyNestedStruct { get; } - - public Nested? NullableNestedStruct { get; set; } - - public struct Nested - { - public string String { get; set; } - public DeeplyNested DeeplyNested { get; set; } - } - - public struct DeeplyNested - { - public int Int32 { get; set; } - public bool Boolean { get; set; } - } - } - - public class BaseClassWithVirtualProperty - { - private string? PrivateProperty { get; set; } - - public virtual string[] Test { get; set; } = System.Array.Empty(); - - public virtual string? TestGetSetOverridden { get; set; } - public virtual string? TestGetOverridden { get; set; } - public virtual string? TestSetOverridden { get; set; } - - private string? _testVirtualSet; - public virtual string? TestVirtualSet - { - set => _testVirtualSet = value; - } - - public virtual string? TestNoOverridden { get; set; } - - public string? ExposePrivatePropertyValue() => PrivateProperty; - } - - public class ClassOverridingVirtualProperty : BaseClassWithVirtualProperty - { - public override string[] Test { get => base.Test; set => base.Test = value; } - - public override string? TestGetSetOverridden { get; set; } - public override string? TestGetOverridden => base.TestGetOverridden; - public override string? TestSetOverridden - { - set => base.TestSetOverridden = value; - } - - private string? _testVirtualSet; - public override string? TestVirtualSet - { - set => _testVirtualSet = value; - } - - public string? ExposeTestVirtualSet() => _testVirtualSet; - } - } -} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj new file mode 100644 index 0000000000000..3b5013751ee77 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj @@ -0,0 +1,44 @@ + + + $(NetCoreAppCurrent);$(NetFrameworkMinimum) + true + true + + + + $(DefineConstants);BUILDING_SOURCE_GENERATOR_TESTS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ILLink.Descriptors.xml b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Tests/ILLink.Descriptors.xml similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ILLink.Descriptors.xml rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Tests/ILLink.Descriptors.xml diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj similarity index 63% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj index 198d6cbfae86b..fc859a6f71bbe 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj @@ -6,11 +6,16 @@ + - + + + + + @@ -19,7 +24,7 @@ - + From 09a086c9b9c4bca1f7e6476221251d821ea2c758 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Mon, 6 Mar 2023 11:25:54 -0800 Subject: [PATCH 2/7] Add baseline tests --- .../tests/SourceGenerators/RoslynTestUtils.cs | 38 ++++- .../Baselines/TestBindCallGen.generated.txt | 71 +++++++++ ...allGen_With_NotSupportedType.generated.txt | 0 .../TestConfigureCallGen.generated.txt | 81 +++++++++++ .../Baselines/TestGetCallGen.generated.txt | 90 ++++++++++++ ...nfingurationBindingSourceGeneratorTests.cs | 136 ++++++++++++++++++ ...ation.Binder.SourceGeneration.Tests.csproj | 20 ++- .../LoggerMessageGeneratorEmitterTests.cs | 26 +--- 8 files changed, 428 insertions(+), 34 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen.generated.txt create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestConfigureCallGen.generated.txt create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestGetCallGen.generated.txt create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/ConfingurationBindingSourceGeneratorTests.cs diff --git a/src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs b/src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs index 3cfa854517371..15bcbeda72794 100644 --- a/src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs +++ b/src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs @@ -266,7 +266,7 @@ public static async Task> RunAnalyzerAndFixer( for (int i = 0; i < count; i++) { SourceText s = await proj.FindDocument(l[i]).GetTextAsync().ConfigureAwait(false); - results.Add(s.ToString().Replace("\r\n", "\n", StringComparison.Ordinal)); + results.Add(Replace(s.ToString(), "\r\n", "\n")); } } else @@ -274,19 +274,43 @@ public static async Task> RunAnalyzerAndFixer( for (int i = 0; i < count; i++) { SourceText s = await proj.FindDocument($"src-{i}.cs").GetTextAsync().ConfigureAwait(false); - results.Add(s.ToString().Replace("\r\n", "\n", StringComparison.Ordinal)); + results.Add(Replace(s.ToString(), "\r\n", "\n")); } } if (extraFile != null) { SourceText s = await proj.FindDocument(extraFile).GetTextAsync().ConfigureAwait(false); - results.Add(s.ToString().Replace("\r\n", "\n", StringComparison.Ordinal)); + results.Add(Replace(s.ToString(), "\r\n", "\n")); } return results; } + public static bool CompareLines(string[] expectedLines, SourceText sourceText, out string message) + { + if (expectedLines.Length != sourceText.Lines.Count) + { + message = string.Format("Line numbers do not match. Expected: {0} lines, but generated {1}", + expectedLines.Length, sourceText.Lines.Count); + return false; + } + int index = 0; + foreach (TextLine textLine in sourceText.Lines) + { + string expectedLine = expectedLines[index]; + if (!expectedLine.Equals(textLine.ToString(), StringComparison.Ordinal)) + { + message = string.Format("Line {0} does not match.{1}Expected Line:{1}{2}{1}Actual Line:{1}{3}", + textLine.LineNumber, Environment.NewLine, expectedLine, textLine); + return false; + } + index++; + } + message = string.Empty; + return true; + } + private static async Task RecreateProjectDocumentsAsync(Project project) { foreach (DocumentId documentId in project.DocumentIds) @@ -304,5 +328,13 @@ private static async Task RecreateDocumentAsync(Document document) SourceText newText = await document.GetTextAsync().ConfigureAwait(false); return document.WithText(SourceText.From(newText.ToString(), newText.Encoding, newText.ChecksumAlgorithm)); } + + private static string Replace(string text, string oldText, string newText) => + text.Replace( + oldText, newText +#if NETCOREAPP + , StringComparison.Ordinal +#endif + ); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen.generated.txt new file mode 100644 index 0000000000000..939ec039aab84 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen.generated.txt @@ -0,0 +1,71 @@ +// +#nullable enable annotations +#nullable disable warnings + +using System.Linq; + +internal static class GeneratedConfigurationBinder +{ + internal static void Bind(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, Program.MyClass obj) => BindCore(configuration, ref obj); + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref Program.MyClass obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + if (configuration.GetSection("MyString").Value is string stringValue0) { obj.MyString = stringValue0; } + if (configuration.GetSection("MyInt").Value is string stringValue1) { obj.MyInt = int.Parse(stringValue1); } + System.Collections.Generic.List temp2 = obj.MyList; + temp2 ??= new System.Collections.Generic.List(); + BindCore(configuration.GetSection("MyList"), ref temp2); + obj.MyList = temp2; + System.Collections.Generic.Dictionary temp3 = obj.MyDictionary; + temp3 ??= new System.Collections.Generic.Dictionary(); + BindCore(configuration.GetSection("MyDictionary"), ref temp3); + obj.MyDictionary = temp3; + } + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.List obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + int element; + foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + if (section?.Value is string stringValue4) + { + element = int.Parse(stringValue4); + obj.Add(element); + } + } + } + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.Dictionary obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + string key; + foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + if (section?.Key is string stringValue5) + { + key = stringValue5; + if (key is not null) + { + string element; + if (section?.Value is string stringValue6) + { + element = stringValue6; + obj[key] = element; + } + } + } + } + } + +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestConfigureCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestConfigureCallGen.generated.txt new file mode 100644 index 0000000000000..e1ab09bb2d8ed --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestConfigureCallGen.generated.txt @@ -0,0 +1,81 @@ +// +#nullable enable annotations +#nullable disable warnings + +using System.Linq; + +internal static class GeneratedConfigurationBinder +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection Configure(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::Microsoft.Extensions.Configuration.IConfiguration configuration) + { + if (typeof(T) == typeof(Program.MyClass)) + { + return services.Configure(obj => + { + BindCore(configuration, ref obj); + }); + } + throw new global::System.NotSupportedException($"Unable to bind to type '{typeof(T)}': 'Generator parser did not detect the type as input'"); + } + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref Program.MyClass obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + if (configuration.GetSection("MyString").Value is string stringValue1) { obj.MyString = stringValue1; } + if (configuration.GetSection("MyInt").Value is string stringValue2) { obj.MyInt = int.Parse(stringValue2); } + System.Collections.Generic.List temp3 = obj.MyList; + temp3 ??= new System.Collections.Generic.List(); + BindCore(configuration.GetSection("MyList"), ref temp3); + obj.MyList = temp3; + System.Collections.Generic.Dictionary temp4 = obj.MyDictionary; + temp4 ??= new System.Collections.Generic.Dictionary(); + BindCore(configuration.GetSection("MyDictionary"), ref temp4); + obj.MyDictionary = temp4; + } + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.List obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + int element; + foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + if (section?.Value is string stringValue5) + { + element = int.Parse(stringValue5); + obj.Add(element); + } + } + } + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.Dictionary obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + string key; + foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + if (section?.Key is string stringValue6) + { + key = stringValue6; + if (key is not null) + { + string element; + if (section?.Value is string stringValue7) + { + element = stringValue7; + obj[key] = element; + } + } + } + } + } + +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestGetCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestGetCallGen.generated.txt new file mode 100644 index 0000000000000..27f12e25827e4 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestGetCallGen.generated.txt @@ -0,0 +1,90 @@ +// +#nullable enable annotations +#nullable disable warnings + +using System.Linq; + +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 (typeof(T) == typeof(Program.MyClass)) + { + Microsoft.Extensions.Configuration.IConfigurationSection? section = configuration as Microsoft.Extensions.Configuration.IConfigurationSection; + if (section?.Value is null && !configuration.GetChildren().Any()) + { + return default; + } + Program.MyClass obj = new Program.MyClass(); + 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'"); + } + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref Program.MyClass obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + if (configuration.GetSection("MyString").Value is string stringValue1) { obj.MyString = stringValue1; } + if (configuration.GetSection("MyInt").Value is string stringValue2) { obj.MyInt = int.Parse(stringValue2); } + System.Collections.Generic.List temp3 = obj.MyList; + temp3 ??= new System.Collections.Generic.List(); + BindCore(configuration.GetSection("MyList"), ref temp3); + obj.MyList = temp3; + System.Collections.Generic.Dictionary temp4 = obj.MyDictionary; + temp4 ??= new System.Collections.Generic.Dictionary(); + BindCore(configuration.GetSection("MyDictionary"), ref temp4); + obj.MyDictionary = temp4; + } + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.List obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + int element; + foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + if (section?.Value is string stringValue5) + { + element = int.Parse(stringValue5); + obj.Add(element); + } + } + } + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.Dictionary obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + string key; + foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + if (section?.Key is string stringValue6) + { + key = stringValue6; + if (key is not null) + { + string element; + if (section?.Value is string stringValue7) + { + element = stringValue7; + obj[key] = element; + } + } + } + } + } + +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/ConfingurationBindingSourceGeneratorTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/ConfingurationBindingSourceGeneratorTests.cs new file mode 100644 index 0000000000000..da778d18fc612 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/ConfingurationBindingSourceGeneratorTests.cs @@ -0,0 +1,136 @@ +// 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; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SourceGenerators.Tests; +using Xunit; + +namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests +{ +#if NETCOREAPP + [ActiveIssue("https://github.com/dotnet/runtime/issues/52062", TestPlatforms.Browser)] + public class ConfingurationBindingSourceGeneratorTests + { + [Fact] + public async Task TestBaseline_TestBindCallGen() + { + string testSourceCode = @" +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +public class Program +{ + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfigurationRoot config = configurationBuilder.Build(); + + MyClass options = new(); + config.Bind(options); + } + + public class MyClass + { + public string MyString { get; set; } + public int MyInt { get; set; } + public List MyList { get; set; } + public Dictionary MyDictionary { get; set; } + } +}"; + + await VerifyAgainstBaselineUsingFile("TestBindCallGen.generated.txt", testSourceCode); + } + + [Fact] + public async Task TestBaseline_TestGetCallGen() + { + string testSourceCode = @" +using System.Collections.Generic; +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 int MyInt { get; set; } + public List MyList { get; set; } + public Dictionary MyDictionary { get; set; } + } +}"; + + await VerifyAgainstBaselineUsingFile("TestGetCallGen.generated.txt", testSourceCode); + } + + [Fact] + public async Task TestBaseline_TestConfigureCallGen() + { + string testSourceCode = @" +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +public class Program +{ + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfiguration config = configurationBuilder.Build(); + IConfigurationSection section = config.GetSection(""MySection""); + + ServiceCollection services = new(); + services.Configure(section); + } + + public class MyClass + { + public string MyString { get; set; } + public int MyInt { get; set; } + public List MyList { get; set; } + public Dictionary MyDictionary { get; set; } + } +}"; + + await VerifyAgainstBaselineUsingFile("TestConfigureCallGen.generated.txt", testSourceCode); + } + + private async Task VerifyAgainstBaselineUsingFile(string filename, string testSourceCode) + { + string baseline = LineEndingsHelper.Normalize(await File.ReadAllTextAsync(Path.Combine("Baselines", filename)).ConfigureAwait(false)); + string[] expectedLines = baseline.Replace("%VERSION%", typeof(ConfigurationBindingSourceGenerator).Assembly.GetName().Version?.ToString()) + .Split(Environment.NewLine); + + var (d, r) = await RoslynTestUtils.RunGenerator( + new ConfigurationBindingSourceGenerator(), + new[] { + typeof(ConfigurationBinder).Assembly, + typeof(IConfiguration).Assembly, + typeof(IServiceCollection).Assembly, + typeof(IDictionary).Assembly, + typeof(ServiceCollection).Assembly, + typeof(OptionsConfigurationServiceCollectionExtensions).Assembly, + }, + new[] { testSourceCode }).ConfigureAwait(false); + + Assert.Empty(d); + Assert.Single(r); + + Assert.True(RoslynTestUtils.CompareLines(expectedLines, r[0].SourceText, + out string errorMessage), errorMessage); + } + } +#endif +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj index 3b5013751ee77..b9f8994352ae3 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj @@ -1,12 +1,13 @@ $(NetCoreAppCurrent);$(NetFrameworkMinimum) + $(MicrosoftCodeAnalysisVersion_4_4) true true - $(DefineConstants);BUILDING_SOURCE_GENERATOR_TESTS + $(DefineConstants);BUILDING_SOURCE_GENERATOR_TESTS;ROSLYN4_0_OR_GREATER;ROSLYN4_4_OR_GREATER @@ -14,10 +15,9 @@ - - + + + @@ -27,13 +27,21 @@ + + - + + + + + + PreserveNewest + diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs index d6fe546d7ff01..b1d767536653f 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs @@ -177,32 +177,8 @@ private async Task VerifyAgainstBaselineUsingFile(string filename, string testSo Assert.Empty(d); Assert.Single(r); - Assert.True(CompareLines(expectedLines, r[0].SourceText, + Assert.True(RoslynTestUtils.CompareLines(expectedLines, r[0].SourceText, out string errorMessage), errorMessage); } - - private bool CompareLines(string[] expectedLines, SourceText sourceText, out string message) - { - if (expectedLines.Length != sourceText.Lines.Count) - { - message = string.Format("Line numbers do not match. Expected: {0} lines, but generated {1}", - expectedLines.Length, sourceText.Lines.Count); - return false; - } - int index = 0; - foreach (TextLine textLine in sourceText.Lines) - { - string expectedLine = expectedLines[index]; - if (!expectedLine.Equals(textLine.ToString(), StringComparison.Ordinal)) - { - message = string.Format("Line {0} does not match.{1}Expected Line:{1}{2}{1}Actual Line:{1}{3}", - textLine.LineNumber, Environment.NewLine, expectedLine, textLine); - return false; - } - index++; - } - message = string.Empty; - return true; - } } } From acbd7cd15d30a97933a80bb8f5c37e5876bb86cf Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Tue, 7 Mar 2023 11:03:32 -0800 Subject: [PATCH 3/7] Rename test project folders --- .../Microsoft.Extensions.Configuration.Binder.sln | 4 ++-- .../Baselines/TestBindCallGen.generated.txt | 0 .../TestBindCallGen_With_NotSupportedType.generated.txt | 0 .../Baselines/TestConfigureCallGen.generated.txt | 0 .../Baselines/TestGetCallGen.generated.txt | 0 .../ConfingurationBindingSourceGeneratorTests.cs | 0 ...ensions.Configuration.Binder.SourceGeneration.Tests.csproj | 0 .../tests/{Tests => UnitTests}/ILLink.Descriptors.xml | 0 .../Microsoft.Extensions.Configuration.Binder.Tests.csproj | 0 9 files changed, 2 insertions(+), 2 deletions(-) rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{SourceGeneration.Tests => SourceGenerationTests}/Baselines/TestBindCallGen.generated.txt (100%) rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{SourceGeneration.Tests => SourceGenerationTests}/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt (100%) rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{SourceGeneration.Tests => SourceGenerationTests}/Baselines/TestConfigureCallGen.generated.txt (100%) rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{SourceGeneration.Tests => SourceGenerationTests}/Baselines/TestGetCallGen.generated.txt (100%) rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{SourceGeneration.Tests => SourceGenerationTests}/ConfingurationBindingSourceGeneratorTests.cs (100%) rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{SourceGeneration.Tests => SourceGenerationTests}/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj (100%) rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{Tests => UnitTests}/ILLink.Descriptors.xml (100%) rename src/libraries/Microsoft.Extensions.Configuration.Binder/tests/{Tests => UnitTests}/Microsoft.Extensions.Configuration.Binder.Tests.csproj (100%) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/Microsoft.Extensions.Configuration.Binder.sln b/src/libraries/Microsoft.Extensions.Configuration.Binder/Microsoft.Extensions.Configuration.Binder.sln index 4961547ca6b05..889dd41ae6d53 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/Microsoft.Extensions.Configuration.Binder.sln +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/Microsoft.Extensions.Configuration.Binder.sln @@ -36,9 +36,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{CC3961B0-C62 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.SourceGeneration", "gen\Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj", "{D4B3EEA1-7394-49EA-A088-897C0CD26D11}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests", "tests\SourceGeneration.Tests\Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj", "{56F4D38E-41A0-45E2-9F04-9E670D002E71}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests", "tests\SourceGenerationTests\Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj", "{56F4D38E-41A0-45E2-9F04-9E670D002E71}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.Tests", "tests\Tests\Microsoft.Extensions.Configuration.Binder.Tests.csproj", "{75BA0154-A24D-421E-9046-C7949DF12A55}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.Tests", "tests\UnitTests\Microsoft.Extensions.Configuration.Binder.Tests.csproj", "{75BA0154-A24D-421E-9046-C7949DF12A55}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen.generated.txt rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestConfigureCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestConfigureCallGen.generated.txt rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestConfigureCallGen.generated.txt diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestGetCallGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Baselines/TestGetCallGen.generated.txt rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestGetCallGen.generated.txt diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/ConfingurationBindingSourceGeneratorTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/ConfingurationBindingSourceGeneratorTests.cs rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGeneration.Tests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Tests/ILLink.Descriptors.xml b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/UnitTests/ILLink.Descriptors.xml similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Tests/ILLink.Descriptors.xml rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/UnitTests/ILLink.Descriptors.xml diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/UnitTests/Microsoft.Extensions.Configuration.Binder.Tests.csproj similarity index 100% rename from src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj rename to src/libraries/Microsoft.Extensions.Configuration.Binder/tests/UnitTests/Microsoft.Extensions.Configuration.Binder.Tests.csproj From 32be0a4e6a8549ad6fb6c4ae530381b0187647ef Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Tue, 7 Mar 2023 13:10:07 -0800 Subject: [PATCH 4/7] Avoid passing entire compilation to generator execute method --- ...figurationBindingSourceGenerator.Parser.cs | 189 +++++------------- .../ConfigurationBindingSourceGenerator.cs | 146 +++++++++++++- 2 files changed, 191 insertions(+), 144 deletions(-) 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 d7ac9cd385b38..003a3f981ffac 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs @@ -2,15 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; -using System.Threading; -using System.Xml.Linq; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.DotnetRuntime.Extensions; using Microsoft.CodeAnalysis.Operations; -using Microsoft.Extensions.Configuration.Binder.SourceGeneration; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { @@ -18,24 +15,8 @@ public sealed partial class ConfigurationBindingSourceGenerator { private sealed class Parser { - private readonly Compilation _compilation; private readonly SourceProductionContext _context; - - private readonly INamedTypeSymbol _symbolForGenericIList; - private readonly INamedTypeSymbol _symbolForICollection; - private readonly INamedTypeSymbol _symbolForIEnumerable; - private readonly INamedTypeSymbol _symbolForString; - - private readonly INamedTypeSymbol? _symbolForConfigurationKeyNameAttribute; - private readonly INamedTypeSymbol? _symbolForDictionary; - private readonly INamedTypeSymbol? _symbolForGenericIDictionary; - private readonly INamedTypeSymbol? _symbolForHashSet; - private readonly INamedTypeSymbol? _symbolForIConfiguration; - private readonly INamedTypeSymbol? _symbolForIConfigurationSection; - private readonly INamedTypeSymbol? _symbolForIDictionary; - private readonly INamedTypeSymbol? _symbolForIServiceCollection; - private readonly INamedTypeSymbol? _symbolForISet; - private readonly INamedTypeSymbol? _symbolForList; + private readonly KnownTypeData _typeData; private readonly HashSet _typesForBindMethodGen = new(); private readonly HashSet _typesForGetMethodGen = new(); @@ -46,75 +27,54 @@ private sealed class Parser private readonly Dictionary _createdSpecs = new(SymbolEqualityComparer.Default); #pragma warning restore RS1024 - public Parser(SourceProductionContext context, Compilation compilation) + public Parser(SourceProductionContext context, in KnownTypeData typeData) { - _compilation = compilation; _context = context; - - _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); + _typeData = typeData; } - public SourceGenerationSpec? GetSourceGenerationSpec( - IEnumerable invocations, - CancellationToken cancellationToken) + public SourceGenerationSpec? GetSourceGenerationSpec(ImmutableArray operations) { - if (_symbolForIConfiguration is null || _symbolForIServiceCollection is null) + if (_typeData.SymbolForIConfiguration is null || _typeData.SymbolForIServiceCollection is null) { return null; } - foreach (InvocationExpressionSyntax invocation in invocations) + foreach (BinderInvocationOperation operation in operations) { - if (IsBindCall(invocation)) - { - ProcessBindCall(invocation, cancellationToken); - } - else if (IsGetCall(invocation)) + switch (operation.BinderMethodKind) { - ProcessGetCall(invocation, cancellationToken); - } - else if (IsConfigureCall(invocation)) - { - ProcessConfigureCall(invocation, cancellationToken); + case BinderMethodKind.Configure: + { + ProcessConfigureCall(operation); + } + break; + case BinderMethodKind.Get: + { + ProcessGetCall(operation); + } + break; + case BinderMethodKind.Bind: + { + ProcessBindCall(operation); + } + break; + default: + break; } } return new SourceGenerationSpec(_typesForBindMethodGen, _typesForGetMethodGen, _typesForConfigureMethodGen); } - public static bool IsInputCall(SyntaxNode node) => - node is not InvocationExpressionSyntax invocation - ? false - : IsBindCall(invocation) || IsConfigureCall(invocation) || IsGetCall(invocation); - - private void ProcessBindCall(InvocationExpressionSyntax invocation, CancellationToken cancellationToken) + private void ProcessBindCall(BinderInvocationOperation binderOperation) { - SemanticModel semanticModel = _compilation.GetSemanticModel(invocation.SyntaxTree); - IInvocationOperation operation = semanticModel.GetOperation(invocation, cancellationToken) as IInvocationOperation; + IInvocationOperation operation = binderOperation.InvocationOperation!; // We're looking for IConfiguration.Bind(object). if (operation is IInvocationOperation { Arguments: { Length: 2 } arguments } && operation.TargetMethod.IsExtensionMethod && - TypesAreEqual(_symbolForIConfiguration, arguments[0].Parameter.Type) && + TypesAreEqual(_typeData.SymbolForIConfiguration, arguments[0].Parameter.Type) && arguments[1].Parameter.Type.SpecialType == SpecialType.System_Object) { IConversionOperation argument = arguments[1].Value as IConversionOperation; @@ -128,7 +88,7 @@ private void ProcessBindCall(InvocationExpressionSyntax invocation, Cancellation return; } - AddTargetConfigType(_typesForBindMethodGen, namedType, invocation.GetLocation()); + AddTargetConfigType(_typesForBindMethodGen, namedType, binderOperation.Location); static ITypeSymbol? ResolveType(IOperation argument) => argument switch @@ -145,16 +105,15 @@ private void ProcessBindCall(InvocationExpressionSyntax invocation, Cancellation } } - private void ProcessGetCall(InvocationExpressionSyntax invocation, CancellationToken cancellationToken) + private void ProcessGetCall(BinderInvocationOperation binderOperation) { - SemanticModel semanticModel = _compilation.GetSemanticModel(invocation.SyntaxTree); - IInvocationOperation? operation = semanticModel.GetOperation(invocation, cancellationToken) as IInvocationOperation; + IInvocationOperation operation = binderOperation.InvocationOperation!; // We're looking for IConfiguration.Get(). if (operation is IInvocationOperation { Arguments.Length: 1 } invocationOperation && invocationOperation.TargetMethod.IsExtensionMethod && invocationOperation.TargetMethod.IsGenericMethod && - TypesAreEqual(_symbolForIConfiguration, invocationOperation.TargetMethod.Parameters[0].Type)) + TypesAreEqual(_typeData.SymbolForIConfiguration, invocationOperation.TargetMethod.Parameters[0].Type)) { ITypeSymbol? type = invocationOperation.TargetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None); if (type is not INamedTypeSymbol { } namedType || @@ -164,21 +123,20 @@ private void ProcessGetCall(InvocationExpressionSyntax invocation, CancellationT return; } - AddTargetConfigType(_typesForGetMethodGen, namedType, invocation.GetLocation()); + AddTargetConfigType(_typesForGetMethodGen, namedType, binderOperation.Location); } } - private void ProcessConfigureCall(InvocationExpressionSyntax invocation, CancellationToken cancellationToken) + private void ProcessConfigureCall(BinderInvocationOperation binderOperation) { - SemanticModel semanticModel = _compilation.GetSemanticModel(invocation.SyntaxTree); - IOperation? operation = semanticModel.GetOperation(invocation, cancellationToken); + IInvocationOperation operation = binderOperation.InvocationOperation!; // We're looking for IServiceCollection.Configure(IConfiguration). if (operation is IInvocationOperation { Arguments.Length: 2 } invocationOperation && invocationOperation.TargetMethod.IsExtensionMethod && invocationOperation.TargetMethod.IsGenericMethod && - TypesAreEqual(_symbolForIServiceCollection, invocationOperation.TargetMethod.Parameters[0].Type) && - TypesAreEqual(_symbolForIConfiguration, invocationOperation.TargetMethod.Parameters[1].Type)) + TypesAreEqual(_typeData.SymbolForIServiceCollection, invocationOperation.TargetMethod.Parameters[0].Type) && + TypesAreEqual(_typeData.SymbolForIConfiguration, invocationOperation.TargetMethod.Parameters[1].Type)) { ITypeSymbol? type = invocationOperation.TargetMethod.TypeArguments[0].WithNullableAnnotation(NullableAnnotation.None); if (type is not INamedTypeSymbol { } namedType || @@ -187,49 +145,10 @@ private void ProcessConfigureCall(InvocationExpressionSyntax invocation, Cancell return; } - AddTargetConfigType(_typesForConfigureMethodGen, namedType, invocation.GetLocation()); + AddTargetConfigType(_typesForConfigureMethodGen, namedType, binderOperation.Location); } } - public static bool IsBindCall(SyntaxNode node) => - node is InvocationExpressionSyntax - { - Expression: MemberAccessExpressionSyntax - { - Name: IdentifierNameSyntax - { - Identifier.ValueText: "Bind" - } - }, - ArgumentList.Arguments.Count: 1 - }; - - public static bool IsConfigureCall(SyntaxNode node) => - node is InvocationExpressionSyntax - { - Expression: MemberAccessExpressionSyntax - { - Name: GenericNameSyntax - { - Identifier.ValueText: "Configure" - } - }, - ArgumentList.Arguments.Count: 1 - }; - - public static bool IsGetCall(SyntaxNode node) => - node is InvocationExpressionSyntax - { - Expression: MemberAccessExpressionSyntax - { - Name: GenericNameSyntax - { - Identifier.ValueText: "Get" - } - }, - ArgumentList.Arguments.Count: 0 - }; - private TypeSpec? AddTargetConfigType(HashSet specs, ITypeSymbol type, Location? location) { if (type is not INamedTypeSymbol namedType || ContainsGenericParameters(namedType)) @@ -282,7 +201,7 @@ node is InvocationExpressionSyntax spec = CreateArraySpec(arrayType, location); return spec == null ? null : CacheSpec(spec); } - else if (TypesAreEqual(type, _symbolForIConfigurationSection)) + else if (TypesAreEqual(type, _typeData.SymbolForIConfigurationSection)) { return CacheSpec(new TypeSpec(type) { Location = location, SpecKind = TypeSpecKind.IConfigurationSection }); } @@ -337,7 +256,7 @@ private bool TryGetTypeSpec(ITypeSymbol type, string unsupportedReason, out Type else { // We want a Bind method for List as a temp holder for the array values. - EnumerableSpec? listSpec = ConstructAndCacheGenericTypeForBind(_symbolForList, arrayType.ElementType) as EnumerableSpec; + EnumerableSpec? listSpec = ConstructAndCacheGenericTypeForBind(_typeData.SymbolForList, arrayType.ElementType) as EnumerableSpec; // We know the element type is supported. Debug.Assert(listSpec != null); @@ -382,10 +301,10 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc } DictionarySpec? concreteType = null; - if (IsInterfaceMatch(type, _symbolForGenericIDictionary) || IsInterfaceMatch(type, _symbolForIDictionary)) + if (IsInterfaceMatch(type, _typeData.SymbolForGenericIDictionary) || IsInterfaceMatch(type, _typeData.SymbolForIDictionary)) { // We know the key and element types are supported. - concreteType = ConstructAndCacheGenericTypeForBind(_symbolForDictionary, keyType, elementType) as DictionarySpec; + concreteType = ConstructAndCacheGenericTypeForBind(_typeData.SymbolForDictionary, keyType, elementType) as DictionarySpec; Debug.Assert(concreteType != null); } else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType, keyType)) @@ -418,14 +337,14 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc } EnumerableSpec? concreteType = null; - if (IsInterfaceMatch(type, _symbolForISet)) + if (IsInterfaceMatch(type, _typeData.SymbolForISet)) { - concreteType = ConstructAndCacheGenericTypeForBind(_symbolForHashSet, elementType) as EnumerableSpec; + concreteType = ConstructAndCacheGenericTypeForBind(_typeData.SymbolForHashSet, elementType) as EnumerableSpec; } - else if (IsInterfaceMatch(type, _symbolForICollection) || - IsInterfaceMatch(type, _symbolForGenericIList)) + else if (IsInterfaceMatch(type, _typeData.SymbolForICollection) || + IsInterfaceMatch(type, _typeData.SymbolForGenericIList)) { - concreteType = ConstructAndCacheGenericTypeForBind(_symbolForList, elementType) as EnumerableSpec; + concreteType = ConstructAndCacheGenericTypeForBind(_typeData.SymbolForList, elementType) as EnumerableSpec; } else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType)) { @@ -468,7 +387,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc } else { - AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => TypesAreEqual(a.AttributeClass, _symbolForConfigurationKeyNameAttribute)); + AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => TypesAreEqual(a.AttributeClass, _typeData.SymbolForConfigurationKeyNameAttribute)); string? configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName; PropertySpec spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName }; @@ -488,7 +407,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc private bool IsCandidateEnumerable(INamedTypeSymbol type, out ITypeSymbol? elementType) { - INamedTypeSymbol? @interface = GetInterface(type, _symbolForICollection); + INamedTypeSymbol? @interface = GetInterface(type, _typeData.SymbolForICollection); if (@interface is not null) { @@ -502,7 +421,7 @@ private bool IsCandidateEnumerable(INamedTypeSymbol type, out ITypeSymbol? eleme private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyType, out ITypeSymbol? elementType) { - INamedTypeSymbol? @interface = GetInterface(type, _symbolForGenericIDictionary); + INamedTypeSymbol? @interface = GetInterface(type, _typeData.SymbolForGenericIDictionary); if (@interface is not null) { keyType = @interface.TypeArguments[0]; @@ -510,10 +429,10 @@ private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyTy return true; } - if (IsInterfaceMatch(type, _symbolForIDictionary)) + if (IsInterfaceMatch(type, _typeData.SymbolForIDictionary)) { - keyType = _symbolForString; - elementType = _symbolForString; + keyType = _typeData.SymbolForString; + elementType = _typeData.SymbolForString; return true; } @@ -523,7 +442,7 @@ private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyTy } private bool IsCollection(INamedTypeSymbol type) => - GetInterface(type, _symbolForIEnumerable) is not null; + GetInterface(type, _typeData.SymbolForIEnumerable) 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 ddf5afaeeacb1..3e290037bed69 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs @@ -3,8 +3,11 @@ //#define LAUNCH_DEBUGGER using System.Collections.Immutable; +using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.DotnetRuntime.Extensions; +using Microsoft.CodeAnalysis.Operations; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { @@ -16,20 +19,23 @@ public sealed partial class ConfigurationBindingSourceGenerator : IIncrementalGe { public void Initialize(IncrementalGeneratorInitializationContext context) { - IncrementalValuesProvider inputCalls = context.SyntaxProvider.CreateSyntaxProvider( - (node, _) => Parser.IsInputCall(node), - (syntaxContext, _) => (InvocationExpressionSyntax)syntaxContext.Node); + IncrementalValueProvider compilationData = + context.CompilationProvider + .Select((compilation, _) => new KnownTypeData(compilation)); - IncrementalValueProvider<(Compilation, ImmutableArray)> compilationAndClasses = - context.CompilationProvider.Combine(inputCalls.Collect()); + IncrementalValuesProvider inputCalls = context.SyntaxProvider.CreateSyntaxProvider( + (node, _) => node is InvocationExpressionSyntax invocation, + (context, cancellationToken) => new BinderInvocationOperation(context, cancellationToken)); - context.RegisterSourceOutput(compilationAndClasses, (spc, source) => Execute(source.Item1, source.Item2, spc)); + IncrementalValueProvider<(KnownTypeData, ImmutableArray)> inputData = compilationData.Combine(inputCalls.Collect()); + + context.RegisterSourceOutput(inputData, (spc, source) => Execute(source.Item1, source.Item2, spc)); } /// /// Generates source code to optimize binding with ConfigurationBinder. /// - private static void Execute(Compilation compilation, ImmutableArray inputCalls, SourceProductionContext context) + private static void Execute(KnownTypeData typeData, ImmutableArray inputCalls, SourceProductionContext context) { #if LAUNCH_DEBUGGER #pragma warning disable IDE0055 @@ -45,8 +51,8 @@ private static void Execute(Compilation compilation, ImmutableArray + invocation is + { + Expression: MemberAccessExpressionSyntax + { + Name: IdentifierNameSyntax + { + Identifier.ValueText: "Bind" + } + }, + ArgumentList.Arguments.Count: 1 + }; + + private static bool IsConfigureCall(InvocationExpressionSyntax invocation) => + invocation is + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + Identifier.ValueText: "Configure" + } + }, + ArgumentList.Arguments.Count: 1 + }; + + private static bool IsGetCall(InvocationExpressionSyntax invocation) => + invocation is + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + Identifier.ValueText: "Get" + } + }, + ArgumentList.Arguments.Count: 0 + }; + } } } From 16e3abe6233062e55125de6559452761b7f8363d Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Tue, 7 Mar 2023 13:50:22 -0800 Subject: [PATCH 5/7] Enable nullable --- .../gen/ConfigurationBindingSourceGenerator.Emitter.cs | 10 +++++----- .../gen/ConfigurationBindingSourceGenerator.Parser.cs | 1 - .../Baselines/TestBindCallGen.generated.txt | 3 +-- ...TestBindCallGen_With_NotSupportedType.generated.txt | 0 .../Baselines/TestConfigureCallGen.generated.txt | 3 +-- .../Baselines/TestGetCallGen.generated.txt | 3 +-- 6 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen_With_NotSupportedType.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 ea1e3c646586b..89c7be3548a00 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs @@ -8,7 +8,6 @@ using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; -using Microsoft.Extensions.Configuration.Binder.SourceGeneration; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { @@ -62,8 +61,7 @@ public Emitter(SourceProductionContext context, SourceGenerationSpec generationS public void Emit() { _writer.WriteLine(@"// -#nullable enable annotations -#nullable disable warnings +#nullable enable using System.Linq; "); @@ -288,8 +286,10 @@ void Emit_BindAndAddLogic_ForElement() } else // For complex types: { + string displayString = elementType.DisplayString + (elementType.IsValueType ? string.Empty : "?"); + // If key already exists, bind to value to existing element instance if not null (for ref types) - string conditionToUseExistingElement = $"if ({Literal.obj}.{Literal.TryGetValue}({Literal.key}, out {elementType.DisplayString} {Literal.element})"; + string conditionToUseExistingElement = $"if ({Literal.obj}.{Literal.TryGetValue}({Literal.key}, out {displayString} {Literal.element})"; conditionToUseExistingElement += !elementType.IsValueType ? $" && {Literal.element} is not {KeyWord.@null})" : ")"; @@ -375,7 +375,7 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert { case TypeSpecKind.System_Object: { - EmitAssignment(expressionForPropertyAccess, expressionForConfigSectionValue); + EmitAssignment(expressionForPropertyAccess, $"{expressionForConfigSectionValue}!"); } break; case TypeSpecKind.StringBasedParse: 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 003a3f981ffac..44077e3706e08 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Linq; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.DotnetRuntime.Extensions; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration 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 939ec039aab84..86b132795986d 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 @@ -1,6 +1,5 @@ // -#nullable enable annotations -#nullable disable warnings +#nullable enable using System.Linq; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen_With_NotSupportedType.generated.txt deleted file mode 100644 index e69de29bb2d1d..0000000000000 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 e1ab09bb2d8ed..0c2b677a571a4 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 @@ -1,6 +1,5 @@ // -#nullable enable annotations -#nullable disable warnings +#nullable enable using System.Linq; 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 27f12e25827e4..0cf35d957db73 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 @@ -1,6 +1,5 @@ // -#nullable enable annotations -#nullable disable warnings +#nullable enable using System.Linq; From 490c00d71f11c145e74280621fb8c2915d855543 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Thu, 9 Mar 2023 11:54:45 -0800 Subject: [PATCH 6/7] Add logic to include generator in nuget package and enable it conditionally --- ...fByDefaultRoslynComponent.targets.template | 17 +++++++++ eng/packaging.targets | 37 ++++++++++++++++++- ...nfiguration.Binder.SourceGeneration.csproj | 1 - ...oft.Extensions.Configuration.Binder.csproj | 11 ++++++ 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 eng/OffByDefaultRoslynComponent.targets.template diff --git a/eng/OffByDefaultRoslynComponent.targets.template b/eng/OffByDefaultRoslynComponent.targets.template new file mode 100644 index 0000000000000..056fa1a376fa3 --- /dev/null +++ b/eng/OffByDefaultRoslynComponent.targets.template @@ -0,0 +1,17 @@ + + + + <_{TargetPrefix}Analyzer Include="@(Analyzer)" Condition="'%(Analyzer.NuGetPackageId)' == '{NuGetPackageId}'" /> + + + + + + + + + + diff --git a/eng/packaging.targets b/eng/packaging.targets index 757188a7ec0b0..9eedd99b7ebc8 100644 --- a/eng/packaging.targets +++ b/eng/packaging.targets @@ -164,7 +164,7 @@ true - + + + <_OffByDefaultRoslynComponentTargetsTemplate>$(MSBuildThisFileDirectory)OffByDefaultRoslynComponent.targets.template + $(IntermediateOutputPath)OffByDefaultRoslynComponent.targets + false + + + + + + + + + + + + + <_OffByDefaultRoslynComponentTargetPrefix>$(PackageId.Replace('.', '_')) + Enable$(PackageId.Replace('.', ''))SourceGenerator + + + + + - $(DefineConstants);BUILDING_SOURCE_GENERATOR $(DefineConstants);LAUNCH_DEBUGGER diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Microsoft.Extensions.Configuration.Binder.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Microsoft.Extensions.Configuration.Binder.csproj index d5415578a356d..15775cfaf0f75 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Microsoft.Extensions.Configuration.Binder.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Microsoft.Extensions.Configuration.Binder.csproj @@ -28,4 +28,15 @@ + + + + + + + false + true + From f4ade8200bb6efd8f0d73d0cd24100e2ca45ad34 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Fri, 10 Mar 2023 17:04:20 -0800 Subject: [PATCH 7/7] Address feedback --- .../tests/SourceGenerators/RoslynTestUtils.cs | 2 +- ...igurationBindingSourceGenerator.Emitter.cs | 62 +++++++------- ...igurationBindingSourceGenerator.Helpers.cs | 8 +- ...figurationBindingSourceGenerator.Parser.cs | 11 +-- .../ConfigurationBindingSourceGenerator.cs | 44 ++++------ ...nfiguration.Binder.SourceGeneration.csproj | 11 +-- .../Baselines/TestBindCallGen.generated.txt | 80 +++++++++++++++---- .../TestConfigureCallGen.generated.txt | 33 +++++--- .../Baselines/TestGetCallGen.generated.txt | 34 +++++--- ...nfingurationBindingSourceGeneratorTests.cs | 3 + ...ation.Binder.SourceGeneration.Tests.csproj | 3 +- 11 files changed, 165 insertions(+), 126 deletions(-) diff --git a/src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs b/src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs index 15bcbeda72794..7c7a3495c99a4 100644 --- a/src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs +++ b/src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs @@ -302,7 +302,7 @@ public static bool CompareLines(string[] expectedLines, SourceText sourceText, o if (!expectedLine.Equals(textLine.ToString(), StringComparison.Ordinal)) { message = string.Format("Line {0} does not match.{1}Expected Line:{1}{2}{1}Actual Line:{1}{3}", - textLine.LineNumber, Environment.NewLine, expectedLine, textLine); + textLine.LineNumber + 1, Environment.NewLine, expectedLine, textLine); return false; } index++; 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 89c7be3548a00..4231c22700375 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs @@ -17,8 +17,9 @@ private sealed partial class Emitter { private static class Expression { - public const string sectionKey = "section?.Key"; - public const string sectionValue = "section?.Value"; + public const string nullableSectionValue = "section?.Value"; + public const string sectionKey = "section.Key"; + public const string sectionValue = "section.Value"; } private static class GlobalName @@ -100,10 +101,12 @@ private void EmitConfigureMethod() _writer.WriteBlockEnd(");"); _writer.WriteBlockEnd(); + _writer.WriteBlankLine(); } Emit_NotSupportedException_UnableToBindType(NotSupportedReason.TypeNotDetectedAsInput); _writer.WriteBlockEnd(); + _writer.WriteBlankLine(); } @@ -116,17 +119,15 @@ private void EmitGetMethod() _writer.WriteBlockStart($"public static T? {Literal.Get}(this {GlobalName.IConfiguration} {Literal.configuration})"); - EmitCheckForNullArgument(Literal.configuration); + EmitCheckForNullArgument_WithBlankLine(Literal.configuration); foreach (TypeSpec type in _generationSpec.TypesForGetMethodGen) { string typeDisplayString = type.DisplayString; _writer.WriteBlockStart($"if (typeof(T) == typeof({typeDisplayString}))"); - EmitBindLogicFromIConfiguration(type, Literal.obj, InitializationKind.Declaration); _writer.WriteLine($"return (T)({GlobalName.Object}){Literal.obj};"); - _writer.WriteBlockEnd(); _writer.WriteBlankLine(); } @@ -172,7 +173,7 @@ private void EmitBindMethod(TypeSpec type) _privateBindCoreMethodGen_QueuedTypes.Enqueue(type); _writer.WriteLine( - @$"internal static void {Literal.Bind}(this {GlobalName.IConfiguration} {Literal.configuration}, {type.DisplayString} {Literal.obj}) => " + + @$"public static void {Literal.Bind}(this {GlobalName.IConfiguration} {Literal.configuration}, {type.DisplayString} {Literal.obj}) => " + $"{Literal.BindCore}({Literal.configuration}, ref {Literal.obj});"); _writer.WriteBlankLine(); } @@ -238,7 +239,7 @@ private void EmitBindCoreImplForArray(EnumerableSpec type) EnumerableSpec concreteType = (type.ConcreteType as EnumerableSpec)!; Debug.Assert(type.SpecKind == TypeSpecKind.Array && type.ConcreteType is not null); - EmitCheckForNullArgumentIfRequired(isValueType: false); + EmitCheckForNullArgument_WithBlankLine_IfRequired(isValueType: false); string tempVarName = GetIncrementalVarName(Literal.temp); @@ -253,7 +254,7 @@ private void EmitBindCoreImplForArray(EnumerableSpec type) private void EmitBindCoreImplForDictionary(DictionarySpec type) { - EmitCheckForNullArgumentIfRequired(type.IsValueType); + EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType); TypeSpec keyType = type.KeyType; TypeSpec elementType = type.ElementType; @@ -271,9 +272,6 @@ private void EmitBindCoreImplForDictionary(DictionarySpec type) void Emit_BindAndAddLogic_ForElement() { - // Validate not null for ref types - if (!keyType.IsValueType) { _writer.WriteBlockStart($"if ({Literal.key} is not {KeyWord.@null})"); } - // For simple types: do regular dictionary add if (elementType.SpecKind == TypeSpecKind.StringBasedParse) { @@ -308,9 +306,6 @@ void EmitBindLogicForElement(InitializationKind initKind) EmitAssignment($"{Literal.obj}[{Literal.key}]", Literal.element); } } - - // End block for key null check - if (!keyType.IsValueType) { _writer.WriteBlockEnd(); } } // End foreach loop. @@ -319,7 +314,7 @@ void EmitBindLogicForElement(InitializationKind initKind) private void EmitBindCoreImplForEnumerable(EnumerableSpec type) { - EmitCheckForNullArgumentIfRequired(type.IsValueType); + EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType); TypeSpec elementType = type.ElementType; @@ -350,23 +345,26 @@ void EmitAddLogicForElement() private void EmitBindCoreImplForObject(ObjectSpec type) { - EmitCheckForNullArgumentIfRequired(type.IsValueType); + EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType); foreach (PropertySpec property in type.Properties) { TypeSpec propertyType = property.Type; EmitBindCoreImplForProperty(property, propertyType, parentType: type); + _writer.WriteBlankLine(); } } private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propertyType, TypeSpec parentType) { string configurationKeyName = property.ConfigurationKeyName; + string propertyParentReference = property.IsStatic ? parentType.DisplayString : Literal.obj; string expressionForPropertyAccess = $"{propertyParentReference}.{property.Name}"; + string expressionForConfigGetSection = $@"{Literal.configuration}.{Literal.GetSection}(""{configurationKeyName}"")"; - string expressionForConfigSectionValue = $"{expressionForConfigGetSection}.{Literal.Value}"; + string expressionForConfigValueIndexer = $@"{Literal.configuration}[""{configurationKeyName}""]"; bool canGet = property.CanGet; bool canSet = property.CanSet; @@ -375,7 +373,7 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert { case TypeSpecKind.System_Object: { - EmitAssignment(expressionForPropertyAccess, $"{expressionForConfigSectionValue}!"); + EmitAssignment(expressionForPropertyAccess, $"{expressionForConfigValueIndexer}!"); } break; case TypeSpecKind.StringBasedParse: @@ -386,7 +384,7 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert EmitBindLogicFromString( propertyType, expressionForPropertyAccess, - expressionForConfigSectionValue); + expressionForConfigValueIndexer); } } break; @@ -438,10 +436,12 @@ private void EmitBindLogicFromIConfiguration(TypeSpec type, string expressionFor if (initKind is InitializationKind.Declaration) { EmitAssignment($"{TypeFullName.IConfigurationSection}? {Literal.section}", $"{Literal.configuration} as {TypeFullName.IConfigurationSection}"); - _writer.WriteBlockStart($"if ({Expression.sectionValue} is null && !{Literal.configuration}.{Literal.GetChildren}().{Literal.Any}())"); + _writer.WriteBlockStart($"if ({Expression.nullableSectionValue} is null && !{Literal.configuration}.{Literal.GetChildren}().{Literal.Any}())"); _writer.WriteLine($"return {KeyWord.@default};"); _writer.WriteBlockEnd(); + _writer.WriteBlankLine(); } + EmitBindCoreCall(type, expressionForMemberAccess, Literal.configuration, initKind); } } @@ -585,17 +585,10 @@ private void EmitBindLogicFromString( return; } - if (writeExtraOnSuccess is null) - { - _writer.WriteLine($"if ({assignmentCondition}) {{ {expressionForMemberAccess} = {rhs}; }}"); - } - else - { - _writer.WriteBlockStart($"if ({assignmentCondition})"); - EmitAssignment(expressionForMemberAccess, rhs); - writeExtraOnSuccess(); - _writer.WriteBlockEnd(); - } + _writer.WriteBlockStart($"if ({assignmentCondition})"); + EmitAssignment(expressionForMemberAccess, rhs); + writeExtraOnSuccess?.Invoke(); + _writer.WriteBlockEnd(); } private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, InitializationKind initKind) @@ -653,19 +646,20 @@ private void EmitCastToIConfigurationSection() private void Emit_NotSupportedException_UnableToBindType(string reason, string typeDisplayString = "{typeof(T)}") => _writer.WriteLine(@$"throw new global::System.NotSupportedException($""{string.Format(ExceptionMessages.TypeNotSupported, typeDisplayString, reason)}"");"); - private void EmitCheckForNullArgumentIfRequired(bool isValueType) + private void EmitCheckForNullArgument_WithBlankLine_IfRequired(bool isValueType) { if (!isValueType) { - EmitCheckForNullArgument(Literal.obj); + EmitCheckForNullArgument_WithBlankLine(Literal.obj); } } - private void EmitCheckForNullArgument(string argName) + private void EmitCheckForNullArgument_WithBlankLine(string argName) { _writer.WriteBlockStart($"if ({argName} is {KeyWord.@null})"); _writer.WriteLine($"throw new global::System.ArgumentNullException(nameof({argName}));"); _writer.WriteBlockEnd(); + _writer.WriteBlankLine(); } private string GetIncrementalVarName(string prefix) => $"{prefix}{_parseValueCount++}"; 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 f0b5e3a3e35dc..6264a7612b005 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs @@ -77,7 +77,6 @@ private static class NotSupportedReason 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 KeyTypeNotSupported = "The collection key 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"; @@ -134,11 +133,8 @@ public void WriteBlockEnd(string? extra = null) public void WriteLine(string source) { - string indentationSource = _indentationLevel == 0 - ? "" - : string.Join("", Enumerable.Repeat(" ", 4 * _indentationLevel)); - - _sb.AppendLine($"{indentationSource}{source}"); + _sb.Append(' ', 4 * _indentationLevel); + _sb.AppendLine(source); } public void WriteBlankLine() => _sb.AppendLine(); 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 44077e3706e08..78db3a5aac912 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs @@ -20,13 +20,10 @@ private sealed class Parser private readonly HashSet _typesForBindMethodGen = new(); private readonly HashSet _typesForGetMethodGen = new(); private readonly HashSet _typesForConfigureMethodGen = new(); - -#pragma warning disable RS1024 private readonly HashSet _unsupportedTypes = new(SymbolEqualityComparer.Default); private readonly Dictionary _createdSpecs = new(SymbolEqualityComparer.Default); -#pragma warning restore RS1024 - public Parser(SourceProductionContext context, in KnownTypeData typeData) + public Parser(SourceProductionContext context, KnownTypeData typeData) { _context = context; _typeData = typeData; @@ -171,10 +168,6 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) return spec; } - if (type.Name == "IDictionary" && type is INamedTypeSymbol { IsGenericType: false }) - { - } - if (type.SpecialType == SpecialType.System_Object) { return CacheSpec(new TypeSpec(type) { Location = location, SpecKind = TypeSpecKind.System_Object }); @@ -287,7 +280,7 @@ private bool TryGetTypeSpec(ITypeSymbol type, string unsupportedReason, out Type private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? location, ITypeSymbol keyType, ITypeSymbol elementType) { - if (!TryGetTypeSpec(keyType, NotSupportedReason.KeyTypeNotSupported, out TypeSpec keySpec) || + if (!TryGetTypeSpec(keyType, NotSupportedReason.DictionaryKeyNotSupported, out TypeSpec keySpec) || !TryGetTypeSpec(elementType, NotSupportedReason.ElementTypeNotSupported, out TypeSpec elementSpec)) { return null; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs index 3e290037bed69..ff1ecaf0cdfea 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.cs @@ -38,13 +38,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) private static void Execute(KnownTypeData typeData, ImmutableArray inputCalls, SourceProductionContext context) { #if LAUNCH_DEBUGGER - #pragma warning disable IDE0055 if (!System.Diagnostics.Debugger.IsAttached) { System.Diagnostics.Debugger.Launch(); } - try - { #endif if (inputCalls.IsDefaultOrEmpty) { @@ -58,34 +55,25 @@ private static void Execute(KnownTypeData typeData, ImmutableArray netstandard2.0 - $(MSBuildThisFileName) - $(MSBuildThisFileName) - SR - FxResources.$(RootNamespace).$(StringResourcesClassName) false - - CS1574 false true cs - 4.4 - $(MicrosoftCodeAnalysisVersion_4_4) - $(DefineConstants);ROSLYN4_0_OR_GREATER;ROSLYN4_4_OR_GREATER @@ -21,7 +12,7 @@ - + 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 86b132795986d..6420c215b3d23 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestBindCallGen.generated.txt @@ -5,7 +5,7 @@ using System.Linq; internal static class GeneratedConfigurationBinder { - internal static void Bind(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, Program.MyClass obj) => BindCore(configuration, ref obj); + public static void Bind(this global::Microsoft.Extensions.Configuration.IConfiguration configuration, Program.MyClass obj) => BindCore(configuration, ref obj); private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref Program.MyClass obj) { @@ -13,16 +13,32 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(obj)); } - if (configuration.GetSection("MyString").Value is string stringValue0) { obj.MyString = stringValue0; } - if (configuration.GetSection("MyInt").Value is string stringValue1) { obj.MyInt = int.Parse(stringValue1); } + + if (configuration["MyString"] is string stringValue0) + { + obj.MyString = stringValue0; + } + + if (configuration["MyInt"] is string stringValue1) + { + obj.MyInt = int.Parse(stringValue1); + } + System.Collections.Generic.List temp2 = obj.MyList; temp2 ??= new System.Collections.Generic.List(); BindCore(configuration.GetSection("MyList"), ref temp2); obj.MyList = temp2; + System.Collections.Generic.Dictionary temp3 = obj.MyDictionary; temp3 ??= new System.Collections.Generic.Dictionary(); BindCore(configuration.GetSection("MyDictionary"), ref temp3); obj.MyDictionary = temp3; + + System.Collections.Generic.Dictionary temp4 = obj.MyComplexDictionary; + temp4 ??= new System.Collections.Generic.Dictionary(); + BindCore(configuration.GetSection("MyComplexDictionary"), ref temp4); + obj.MyComplexDictionary = temp4; + } private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.List obj) @@ -31,12 +47,13 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(obj)); } + int element; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section?.Value is string stringValue4) + if (section.Value is string stringValue5) { - element = int.Parse(stringValue4); + element = int.Parse(stringValue5); obj.Add(element); } } @@ -48,23 +65,58 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(obj)); } + + string key; + foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) + { + if (section.Key is string stringValue6) + { + key = stringValue6; + string element; + if (section.Value is string stringValue7) + { + element = stringValue7; + obj[key] = element; + } + } + } + } + + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.Dictionary obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + string key; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section?.Key is string stringValue5) + if (section.Key is string stringValue8) { - key = stringValue5; - if (key is not null) + key = stringValue8; + if (obj.TryGetValue(key, out Program.MyClass2? element) && element is not null) { - string element; - if (section?.Value is string stringValue6) - { - element = stringValue6; - obj[key] = element; - } + BindCore(section, ref element); + obj[key] = element; + } + else + { + element = new Program.MyClass2(); + BindCore(section, ref element); + obj[key] = element; } } } } + private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref Program.MyClass2 obj) + { + if (obj is null) + { + throw new global::System.ArgumentNullException(nameof(obj)); + } + + } + } 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 0c2b677a571a4..cd255ce893cd8 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 @@ -14,6 +14,7 @@ internal static class GeneratedConfigurationBinder BindCore(configuration, ref obj); }); } + throw new global::System.NotSupportedException($"Unable to bind to type '{typeof(T)}': 'Generator parser did not detect the type as input'"); } @@ -23,16 +24,27 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(obj)); } - if (configuration.GetSection("MyString").Value is string stringValue1) { obj.MyString = stringValue1; } - if (configuration.GetSection("MyInt").Value is string stringValue2) { obj.MyInt = int.Parse(stringValue2); } + + if (configuration["MyString"] is string stringValue1) + { + obj.MyString = stringValue1; + } + + if (configuration["MyInt"] is string stringValue2) + { + obj.MyInt = int.Parse(stringValue2); + } + System.Collections.Generic.List temp3 = obj.MyList; temp3 ??= new System.Collections.Generic.List(); BindCore(configuration.GetSection("MyList"), ref temp3); obj.MyList = temp3; + System.Collections.Generic.Dictionary temp4 = obj.MyDictionary; temp4 ??= new System.Collections.Generic.Dictionary(); BindCore(configuration.GetSection("MyDictionary"), ref temp4); obj.MyDictionary = temp4; + } private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.List obj) @@ -41,10 +53,11 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(obj)); } + int element; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section?.Value is string stringValue5) + if (section.Value is string stringValue5) { element = int.Parse(stringValue5); obj.Add(element); @@ -58,20 +71,18 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(obj)); } + string key; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section?.Key is string stringValue6) + if (section.Key is string stringValue6) { key = stringValue6; - if (key is not null) + string element; + if (section.Value is string stringValue7) { - string element; - if (section?.Value is string stringValue7) - { - element = stringValue7; - obj[key] = element; - } + element = stringValue7; + obj[key] = element; } } } 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 0cf35d957db73..bddaf65a9bd8e 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 @@ -11,6 +11,7 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(configuration)); } + if (typeof(T) == typeof(Program.MyClass)) { Microsoft.Extensions.Configuration.IConfigurationSection? section = configuration as Microsoft.Extensions.Configuration.IConfigurationSection; @@ -18,6 +19,7 @@ internal static class GeneratedConfigurationBinder { return default; } + Program.MyClass obj = new Program.MyClass(); BindCore(configuration, ref obj); return (T)(object)obj; @@ -32,16 +34,27 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(obj)); } - if (configuration.GetSection("MyString").Value is string stringValue1) { obj.MyString = stringValue1; } - if (configuration.GetSection("MyInt").Value is string stringValue2) { obj.MyInt = int.Parse(stringValue2); } + + if (configuration["MyString"] is string stringValue1) + { + obj.MyString = stringValue1; + } + + if (configuration["MyInt"] is string stringValue2) + { + obj.MyInt = int.Parse(stringValue2); + } + System.Collections.Generic.List temp3 = obj.MyList; temp3 ??= new System.Collections.Generic.List(); BindCore(configuration.GetSection("MyList"), ref temp3); obj.MyList = temp3; + System.Collections.Generic.Dictionary temp4 = obj.MyDictionary; temp4 ??= new System.Collections.Generic.Dictionary(); BindCore(configuration.GetSection("MyDictionary"), ref temp4); obj.MyDictionary = temp4; + } private static void BindCore(global::Microsoft.Extensions.Configuration.IConfiguration configuration, ref System.Collections.Generic.List obj) @@ -50,10 +63,11 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(obj)); } + int element; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section?.Value is string stringValue5) + if (section.Value is string stringValue5) { element = int.Parse(stringValue5); obj.Add(element); @@ -67,20 +81,18 @@ internal static class GeneratedConfigurationBinder { throw new global::System.ArgumentNullException(nameof(obj)); } + string key; foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren()) { - if (section?.Key is string stringValue6) + if (section.Key is string stringValue6) { key = stringValue6; - if (key is not null) + string element; + if (section.Value is string stringValue7) { - string element; - if (section?.Value is string stringValue7) - { - element = stringValue7; - obj[key] = element; - } + element = stringValue7; + obj[key] = element; } } } 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 da778d18fc612..eaa8ec0d4465a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs @@ -39,7 +39,10 @@ public class MyClass public int MyInt { get; set; } public List MyList { get; set; } public Dictionary MyDictionary { get; set; } + public Dictionary MyComplexDictionary { get; set; } } + + public class MyClass2 { } }"; await VerifyAgainstBaselineUsingFile("TestBindCallGen.generated.txt", testSourceCode); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj index b9f8994352ae3..e5a0337680526 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj @@ -1,7 +1,6 @@ $(NetCoreAppCurrent);$(NetFrameworkMinimum) - $(MicrosoftCodeAnalysisVersion_4_4) true true @@ -27,7 +26,7 @@ - +