diff --git a/eng/Build.props b/eng/Build.props
index 58deebde93f9..4c0542db15da 100644
--- a/eng/Build.props
+++ b/eng/Build.props
@@ -109,6 +109,7 @@
$(RepoRoot)src\Components\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj;
$(RepoRoot)src\SignalR\**\*.npmproj;
$(RepoRoot)src\Middleware\**\*.npmproj;
+ $(RepoRoot)src\JSInterop\**\*.npmproj;
"
RestoreInParallel="false"
Exclude="@(ProjectToExclude)" />
@@ -152,6 +153,14 @@
$(RepoRoot)src\SignalR\**\*.csproj;
$(RepoRoot)src\Components\**\*.csproj;
$(RepoRoot)src\Analyzers\**\*.csproj;
+ $(RepoRoot)src\FileProviders\**\*.csproj;
+ $(RepoRoot)src\Configuration.KeyPerFile\**\*.csproj;
+ $(RepoRoot)src\Localization\**\*.csproj;
+ $(RepoRoot)src\ObjectPool\**\*.csproj;
+ $(RepoRoot)src\JSInterop\**\*.csproj;
+ $(RepoRoot)src\WebEncoders\**\*.csproj;
+ $(RepoRoot)src\HealthChecks\**\*.csproj;
+ $(RepoRoot)src\Testing\**\*.csproj;
$(RepoRoot)src\ProjectTemplates\*\*.csproj;
$(RepoRoot)src\ProjectTemplates\testassets\*\*.csproj;
"
@@ -181,6 +190,14 @@
$(RepoRoot)src\Azure\**\src\*.csproj;
$(RepoRoot)src\SignalR\**\src\*.csproj;
$(RepoRoot)src\Components\**\src\*.csproj;
+ $(RepoRoot)src\FileProviders\**\src\*.csproj;
+ $(RepoRoot)src\Configuration.KeyPerFile\**\src\*.csproj;
+ $(RepoRoot)src\Localization\**\src\*.csproj;
+ $(RepoRoot)src\ObjectPool\**\src\*.csproj;
+ $(RepoRoot)src\JSInterop\**\src\*.csproj;
+ $(RepoRoot)src\WebEncoders\**\src\*.csproj;
+ $(RepoRoot)src\HealthChecks\**\src\*.csproj;
+ $(RepoRoot)src\Testing\**\src\*.csproj;
"
Exclude="
@(ProjectToBuild);
diff --git a/eng/Dependencies.props b/eng/Dependencies.props
index ae4d297ec8c5..0603add6c39e 100644
--- a/eng/Dependencies.props
+++ b/eng/Dependencies.props
@@ -30,10 +30,8 @@ and are generated based on the last package release.
-
-
@@ -41,7 +39,6 @@ and are generated based on the last package release.
-
@@ -49,20 +46,13 @@ and are generated based on the last package release.
-
-
-
-
-
-
-
@@ -71,16 +61,10 @@ and are generated based on the last package release.
-
-
-
-
-
-
@@ -94,14 +78,17 @@ and are generated based on the last package release.
+
+
+
@@ -122,9 +109,7 @@ and are generated based on the last package release.
-
-
diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props
index feebe6e3ffcf..f71e3d1d72a7 100644
--- a/eng/ProjectReferences.props
+++ b/eng/ProjectReferences.props
@@ -63,6 +63,7 @@
+
@@ -143,5 +144,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/eng/SharedFramework.External.props b/eng/SharedFramework.External.props
index ce3b5b7d334c..9812152edd7b 100644
--- a/eng/SharedFramework.External.props
+++ b/eng/SharedFramework.External.props
@@ -30,24 +30,18 @@
-
-
-
-
-
-
@@ -56,13 +50,10 @@
-
-
-
diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props
index ddfe60ecfb73..49d28f78f0f2 100644
--- a/eng/SharedFramework.Local.props
+++ b/eng/SharedFramework.Local.props
@@ -26,6 +26,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 60a7f0682726..687aea17bb0d 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -61,14 +61,6 @@
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
@@ -85,10 +77,6 @@
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
@@ -121,10 +109,6 @@
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
@@ -149,14 +133,6 @@
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
@@ -165,10 +141,6 @@
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
@@ -177,10 +149,6 @@
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
@@ -189,22 +157,10 @@
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
@@ -245,10 +201,6 @@
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
@@ -261,34 +213,14 @@
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/extensions
396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/runtime
0f3f8e1930c28b67f29990126bc2e8527e959a2e
@@ -416,10 +348,6 @@
https://github.com/dotnet/arcade
f50767f96246063f33a6565d318f3adf9058bace
-
- https://github.com/dotnet/extensions
- 396aff55e0b4628a7a44375e4b72e5d19a6e37ab
-
https://github.com/dotnet/roslyn
92790e24cc2b9f9e336ed0365d610e106c01df88
diff --git a/eng/Versions.props b/eng/Versions.props
index 8c8f2c741621..5abc0e037674 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -98,14 +98,10 @@
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
@@ -114,27 +110,19 @@
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
@@ -145,17 +133,11 @@
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.1.20114.1
- 5.0.0-preview.1.20114.1
5.0.0-preview.2.20120.8
5.0.0-preview.2.20120.8
@@ -198,7 +180,9 @@
0.3.0-alpha.19317.1
4.3.0
4.3.2
+ 4.3.0
4.5.3
+ 4.5.0
1.10.0
5.2.6
diff --git a/eng/targets/ResolveReferences.targets b/eng/targets/ResolveReferences.targets
index 9762a9380ec5..37a17b714034 100644
--- a/eng/targets/ResolveReferences.targets
+++ b/eng/targets/ResolveReferences.targets
@@ -119,7 +119,7 @@
-
diff --git a/src/Components/Blazor/Build/test/Microsoft.AspNetCore.Blazor.Build.Tests.csproj b/src/Components/Blazor/Build/test/Microsoft.AspNetCore.Blazor.Build.Tests.csproj
index bc90f9596ecf..56eccd10909b 100644
--- a/src/Components/Blazor/Build/test/Microsoft.AspNetCore.Blazor.Build.Tests.csproj
+++ b/src/Components/Blazor/Build/test/Microsoft.AspNetCore.Blazor.Build.Tests.csproj
@@ -31,7 +31,6 @@
-
@@ -40,6 +39,7 @@
+
diff --git a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj
index 4749a781cabe..366a1bce5efd 100644
--- a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj
+++ b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj
@@ -18,8 +18,9 @@
-
+
+
diff --git a/src/Components/Components/perf/Microsoft.AspNetCore.Components.Performance.csproj b/src/Components/Components/perf/Microsoft.AspNetCore.Components.Performance.csproj
index a893d64abd8e..f40a0a40987a 100644
--- a/src/Components/Components/perf/Microsoft.AspNetCore.Components.Performance.csproj
+++ b/src/Components/Components/perf/Microsoft.AspNetCore.Components.Performance.csproj
@@ -10,7 +10,8 @@
-
+
+
diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/ref/Microsoft.AspNetCore.Components.csproj
index c535b143b6db..50f888c26a64 100644
--- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.csproj
+++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.csproj
@@ -10,7 +10,6 @@
-
@@ -19,6 +18,5 @@
-
diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
index 6bc25e8d43aa..fefadced4988 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
@@ -15,7 +15,8 @@
-
+
+
@@ -33,12 +34,16 @@
+ Projects="
+ ../../Analyzers/src/Microsoft.AspNetCore.Components.Analyzers.csproj;
+ ../../../JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj;
+ ../../../Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj">
+
@@ -50,7 +55,6 @@
-
diff --git a/src/Components/Server/src/Directory.Build.targets b/src/Components/Server/src/Directory.Build.targets
new file mode 100644
index 000000000000..09953b9b6c9d
--- /dev/null
+++ b/src/Components/Server/src/Directory.Build.targets
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
index 69bf60373e41..98fa33ca3e84 100644
--- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
+++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj
@@ -1,5 +1,4 @@
-
$(DefaultNetCoreTargetFramework)
Runtime server features for ASP.NET Core Components.
@@ -10,6 +9,7 @@
CS0436;$(NoWarn)
$(DefineConstants);ENABLE_UNSAFE_MSGPACK;SPAN_BUILTIN;MESSAGEPACK_INTERNAL;COMPONENTS_SERVER
false
+ Microsoft.Extensions.FileProviders.Embedded.Manifest.xml
@@ -22,7 +22,8 @@
-
+
+
@@ -39,6 +40,15 @@
$(RepoRoot)src\submodules\MessagePack-CSharp\src\MessagePack.UnityClient\Assets\Scripts\MessagePack\
+
+
+ <_FileProviderTaskAssembly>$(ArtifactsDir)bin\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task\$(Configuration)\netstandard2.0\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.dll
+
+
+
+
@@ -84,5 +94,4 @@
-
\ No newline at end of file
diff --git a/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.csproj b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.csproj
new file mode 100644
index 000000000000..b554afa71551
--- /dev/null
+++ b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0;$(DefaultNetCoreTargetFramework)
+ $(DefaultNetCoreTargetFramework)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netcoreapp.cs b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netcoreapp.cs
new file mode 100644
index 000000000000..d6e800587ad9
--- /dev/null
+++ b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netcoreapp.cs
@@ -0,0 +1,34 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.Extensions.Configuration
+{
+ public static partial class KeyPerFileConfigurationBuilderExtensions
+ {
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action configureSource) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange) { throw null; }
+ }
+}
+namespace Microsoft.Extensions.Configuration.KeyPerFile
+{
+ public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
+ {
+ public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
+ public void Dispose() { }
+ public override void Load() { }
+ public override string ToString() { throw null; }
+ }
+ public partial class KeyPerFileConfigurationSource : Microsoft.Extensions.Configuration.IConfigurationSource
+ {
+ public KeyPerFileConfigurationSource() { }
+ public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public System.Func IgnoreCondition { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public string IgnorePrefix { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public bool Optional { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public int ReloadDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public bool ReloadOnChange { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs
new file mode 100644
index 000000000000..d6e800587ad9
--- /dev/null
+++ b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs
@@ -0,0 +1,34 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.Extensions.Configuration
+{
+ public static partial class KeyPerFileConfigurationBuilderExtensions
+ {
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action configureSource) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange) { throw null; }
+ }
+}
+namespace Microsoft.Extensions.Configuration.KeyPerFile
+{
+ public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
+ {
+ public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
+ public void Dispose() { }
+ public override void Load() { }
+ public override string ToString() { throw null; }
+ }
+ public partial class KeyPerFileConfigurationSource : Microsoft.Extensions.Configuration.IConfigurationSource
+ {
+ public KeyPerFileConfigurationSource() { }
+ public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public System.Func IgnoreCondition { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public string IgnorePrefix { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public bool Optional { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public int ReloadDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public bool ReloadOnChange { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs
new file mode 100644
index 000000000000..e4c8dd58eea5
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs
@@ -0,0 +1,65 @@
+using System;
+using System.IO;
+using Microsoft.Extensions.Configuration.KeyPerFile;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.Extensions.Configuration
+{
+ ///
+ /// Extension methods for registering with .
+ ///
+ public static class KeyPerFileConfigurationBuilderExtensions
+ {
+ ///
+ /// Adds configuration using files from a directory. File names are used as the key,
+ /// file contents are used as the value.
+ ///
+ /// The to add to.
+ /// The path to the directory.
+ /// The .
+ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath)
+ => builder.AddKeyPerFile(directoryPath, optional: false, reloadOnChange: false);
+
+ ///
+ /// Adds configuration using files from a directory. File names are used as the key,
+ /// file contents are used as the value.
+ ///
+ /// The to add to.
+ /// The path to the directory.
+ /// Whether the directory is optional.
+ /// The .
+ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional)
+ => builder.AddKeyPerFile(directoryPath, optional, reloadOnChange: false);
+
+ ///
+ /// Adds configuration using files from a directory. File names are used as the key,
+ /// file contents are used as the value.
+ ///
+ /// The to add to.
+ /// The path to the directory.
+ /// Whether the directory is optional.
+ /// Whether the configuration should be reloaded if the files are changed, added or removed.
+ /// The .
+ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange)
+ => builder.AddKeyPerFile(source =>
+ {
+ // Only try to set the file provider if its not optional or the directory exists
+ if (!optional || Directory.Exists(directoryPath))
+ {
+ source.FileProvider = new PhysicalFileProvider(directoryPath);
+ }
+ source.Optional = optional;
+ source.ReloadOnChange = reloadOnChange;
+ });
+
+ ///
+ /// Adds configuration using files from a directory. File names are used as the key,
+ /// file contents are used as the value.
+ ///
+ /// The to add to.
+ /// Configures the source.
+ /// The .
+ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, Action configureSource)
+ => builder.Add(configureSource);
+ }
+}
diff --git a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs
new file mode 100644
index 000000000000..f586896fc275
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.Extensions.Configuration.KeyPerFile
+{
+ ///
+ /// A that uses a directory's files as configuration key/values.
+ ///
+ public class KeyPerFileConfigurationProvider : ConfigurationProvider, IDisposable
+ {
+ private readonly IDisposable _changeTokenRegistration;
+
+ KeyPerFileConfigurationSource Source { get; set; }
+
+ ///
+ /// Initializes a new instance.
+ ///
+ /// The settings.
+ public KeyPerFileConfigurationProvider(KeyPerFileConfigurationSource source)
+ {
+ Source = source ?? throw new ArgumentNullException(nameof(source));
+
+ if (Source.ReloadOnChange && Source.FileProvider != null)
+ {
+ _changeTokenRegistration = ChangeToken.OnChange(
+ () => Source.FileProvider.Watch("*"),
+ () =>
+ {
+ Thread.Sleep(Source.ReloadDelay);
+ Load(reload: true);
+ });
+ }
+
+ }
+
+ private static string NormalizeKey(string key)
+ => key.Replace("__", ConfigurationPath.KeyDelimiter);
+
+ private static string TrimNewLine(string value)
+ => value.EndsWith(Environment.NewLine)
+ ? value.Substring(0, value.Length - Environment.NewLine.Length)
+ : value;
+
+ ///
+ /// Loads the configuration values.
+ ///
+ public override void Load()
+ {
+ Load(reload: false);
+ }
+
+ private void Load(bool reload)
+ {
+ var data = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ if (Source.FileProvider == null)
+ {
+ if (Source.Optional || reload) // Always optional on reload
+ {
+ Data = data;
+ return;
+ }
+
+ throw new DirectoryNotFoundException("A non-null file provider for the directory is required when this source is not optional.");
+ }
+
+ var directory = Source.FileProvider.GetDirectoryContents("/");
+ if (!directory.Exists)
+ {
+ if (Source.Optional || reload) // Always optional on reload
+ {
+ Data = data;
+ return;
+ }
+ throw new DirectoryNotFoundException("The root directory for the FileProvider doesn't exist and is not optional.");
+ }
+ else
+ {
+ foreach (var file in directory)
+ {
+ if (file.IsDirectory)
+ {
+ continue;
+ }
+
+ using var stream = file.CreateReadStream();
+ using var streamReader = new StreamReader(stream);
+
+ if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name))
+ {
+ data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
+ }
+
+ }
+ }
+
+ Data = data;
+ }
+
+ private string GetDirectoryName()
+ => Source.FileProvider?.GetFileInfo("/")?.PhysicalPath ?? "";
+
+ ///
+ /// Generates a string representing this provider name and relevant details.
+ ///
+ /// The configuration name.
+ public override string ToString()
+ => $"{GetType().Name} for files in '{GetDirectoryName()}' ({(Source.Optional ? "Optional" : "Required")})";
+
+ ///
+ public void Dispose()
+ {
+ _changeTokenRegistration?.Dispose();
+ }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs
new file mode 100644
index 000000000000..2b64d5a8dda7
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs
@@ -0,0 +1,59 @@
+using System;
+using System.IO;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.Extensions.Configuration.KeyPerFile
+{
+ ///
+ /// An used to configure .
+ ///
+ public class KeyPerFileConfigurationSource : IConfigurationSource
+ {
+ ///
+ /// Constructor;
+ ///
+ public KeyPerFileConfigurationSource()
+ => IgnoreCondition = s => IgnorePrefix != null && s.StartsWith(IgnorePrefix);
+
+ ///
+ /// The FileProvider whos root "/" directory files will be used as configuration data.
+ ///
+ public IFileProvider FileProvider { get; set; }
+
+ ///
+ /// Files that start with this prefix will be excluded.
+ /// Defaults to "ignore.".
+ ///
+ public string IgnorePrefix { get; set; } = "ignore.";
+
+ ///
+ /// Used to determine if a file should be ignored using its name.
+ /// Defaults to using the IgnorePrefix.
+ ///
+ public Func IgnoreCondition { get; set; }
+
+ ///
+ /// If false, will throw if the directory doesn't exist.
+ ///
+ public bool Optional { get; set; }
+
+ ///
+ /// Determines whether the source will be loaded if the underlying file changes.
+ ///
+ public bool ReloadOnChange { get; set; }
+
+ ///
+ /// Number of milliseconds that reload will wait before calling Load. This helps
+ /// avoid triggering reload before a file is completely written. Default is 250.
+ ///
+ public int ReloadDelay { get; set; } = 250;
+
+ ///
+ /// Builds the for this source.
+ ///
+ /// The .
+ /// A
+ public IConfigurationProvider Build(IConfigurationBuilder builder)
+ => new KeyPerFileConfigurationProvider(this);
+ }
+}
diff --git a/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj b/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj
new file mode 100644
index 000000000000..af1ecb684fe7
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Configuration provider that uses files in a directory for Microsoft.Extensions.Configuration.
+ netstandard2.0;$(DefaultNetCoreTargetFramework)
+ $(DefaultNetCoreTargetFramework)
+ true
+ true
+ true
+ configuration
+ $(NoWarn);PKG0001
+
+
+
+
+
+
+
+
diff --git a/src/Configuration.KeyPerFile/src/README.md b/src/Configuration.KeyPerFile/src/README.md
new file mode 100644
index 000000000000..29952e9139dc
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/README.md
@@ -0,0 +1,2 @@
+
+This is a configuration provider that uses a directory's files as data. A file's name is the key and the contents are the value.
diff --git a/src/Configuration.KeyPerFile/test/ConfigurationProviderCommandLineTest.cs b/src/Configuration.KeyPerFile/test/ConfigurationProviderCommandLineTest.cs
new file mode 100644
index 000000000000..066aecf33756
--- /dev/null
+++ b/src/Configuration.KeyPerFile/test/ConfigurationProviderCommandLineTest.cs
@@ -0,0 +1,43 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Configuration.Test;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
+{
+ public class ConfigurationProviderCommandLineTest : ConfigurationProviderTestBase
+ {
+ protected override (IConfigurationProvider Provider, Action Initializer) LoadThroughProvider(
+ TestSection testConfig)
+ {
+ var testFiles = new List();
+ SectionToTestFiles(testFiles, "", testConfig);
+
+ var provider = new KeyPerFileConfigurationProvider(
+ new KeyPerFileConfigurationSource
+ {
+ Optional = true,
+ FileProvider = new TestFileProvider(testFiles.ToArray())
+ });
+
+ return (provider, () => { });
+ }
+
+ private void SectionToTestFiles(List testFiles, string sectionName, TestSection section)
+ {
+ foreach (var tuple in section.Values.SelectMany(e => e.Value.Expand(e.Key)))
+ {
+ testFiles.Add(new TestFile(sectionName + tuple.Key, tuple.Value));
+ }
+
+ foreach (var tuple in section.Sections)
+ {
+ SectionToTestFiles(testFiles, sectionName + tuple.Key + "__", tuple.Section);
+ }
+ }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/test/ConfigurationProviderTestBase.cs b/src/Configuration.KeyPerFile/test/ConfigurationProviderTestBase.cs
new file mode 100644
index 000000000000..67cb20c6c380
--- /dev/null
+++ b/src/Configuration.KeyPerFile/test/ConfigurationProviderTestBase.cs
@@ -0,0 +1,761 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Configuration.Memory;
+using Xunit;
+
+namespace Microsoft.Extensions.Configuration.Test
+{
+ public abstract class ConfigurationProviderTestBase
+ {
+ [Fact]
+ public virtual void Load_from_single_provider()
+ {
+ var configRoot = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig));
+
+ AssertConfig(configRoot);
+ }
+
+ [Fact]
+ public virtual void Has_debug_view()
+ {
+ var configRoot = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig));
+ var providerTag = configRoot.Providers.Single().ToString();
+
+ var expected =
+ $@"Key1=Value1 ({providerTag})
+Section1:
+ Key2=Value12 ({providerTag})
+ Section2:
+ Key3=Value123 ({providerTag})
+ Key3a:
+ 0=ArrayValue0 ({providerTag})
+ 1=ArrayValue1 ({providerTag})
+ 2=ArrayValue2 ({providerTag})
+Section3:
+ Section4:
+ Key4=Value344 ({providerTag})
+";
+
+ AssertDebugView(configRoot, expected);
+ }
+
+ [Fact]
+ public virtual void Null_values_are_included_in_the_config()
+ {
+ AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.NullsTestConfig)), expectNulls: true, nullValue: "");
+ }
+
+ [Fact]
+ public virtual void Combine_after_other_provider()
+ {
+ AssertConfig(
+ BuildConfigRoot(
+ LoadUsingMemoryProvider(TestSection.MissingSection2ValuesConfig),
+ LoadThroughProvider(TestSection.MissingSection4Config)));
+
+ AssertConfig(
+ BuildConfigRoot(
+ LoadUsingMemoryProvider(TestSection.MissingSection4Config),
+ LoadThroughProvider(TestSection.MissingSection2ValuesConfig)));
+ }
+
+ [Fact]
+ public virtual void Combine_before_other_provider()
+ {
+ AssertConfig(
+ BuildConfigRoot(
+ LoadThroughProvider(TestSection.MissingSection2ValuesConfig),
+ LoadUsingMemoryProvider(TestSection.MissingSection4Config)));
+
+ AssertConfig(
+ BuildConfigRoot(
+ LoadThroughProvider(TestSection.MissingSection4Config),
+ LoadUsingMemoryProvider(TestSection.MissingSection2ValuesConfig)));
+ }
+
+ [Fact]
+ public virtual void Second_provider_overrides_values_from_first()
+ {
+ AssertConfig(
+ BuildConfigRoot(
+ LoadUsingMemoryProvider(TestSection.NoValuesTestConfig),
+ LoadThroughProvider(TestSection.TestConfig)));
+ }
+
+ [Fact]
+ public virtual void Combining_from_multiple_providers_is_case_insensitive()
+ {
+ AssertConfig(
+ BuildConfigRoot(
+ LoadUsingMemoryProvider(TestSection.DifferentCasedTestConfig),
+ LoadThroughProvider(TestSection.TestConfig)));
+ }
+
+ [Fact]
+ public virtual void Load_from_single_provider_with_duplicates_throws()
+ {
+ AssertFormatOrArgumentException(
+ () => BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesTestConfig)));
+ }
+
+ [Fact]
+ public virtual void Load_from_single_provider_with_differing_case_duplicates_throws()
+ {
+ AssertFormatOrArgumentException(
+ () => BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesDifferentCaseTestConfig)));
+ }
+
+ private void AssertFormatOrArgumentException(Action test)
+ {
+ Exception caught = null;
+ try
+ {
+ test();
+ }
+ catch (Exception e)
+ {
+ caught = e;
+ }
+
+ Assert.True(caught is ArgumentException
+ || caught is FormatException);
+ }
+
+ [Fact]
+ public virtual void Bind_to_object()
+ {
+ var configuration = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig));
+
+ var options = configuration.Get();
+
+ Assert.Equal("Value1", options.Key1);
+ Assert.Equal("Value12", options.Section1.Key2);
+ Assert.Equal("Value123", options.Section1.Section2.Key3);
+ Assert.Equal("Value344", options.Section3.Section4.Key4);
+ Assert.Equal(new[] { "ArrayValue0", "ArrayValue1", "ArrayValue2" }, options.Section1.Section2.Key3a);
+ }
+
+ public class AsOptions
+ {
+ public string Key1 { get; set; }
+
+ public Section1AsOptions Section1 { get; set; }
+ public Section3AsOptions Section3 { get; set; }
+ }
+
+ public class Section1AsOptions
+ {
+ public string Key2 { get; set; }
+
+ public Section2AsOptions Section2 { get; set; }
+ }
+
+ public class Section2AsOptions
+ {
+ public string Key3 { get; set; }
+ public string[] Key3a { get; set; }
+ }
+
+ public class Section3AsOptions
+ {
+ public Section4AsOptions Section4 { get; set; }
+ }
+
+ public class Section4AsOptions
+ {
+ public string Key4 { get; set; }
+ }
+
+ protected virtual void AssertDebugView(
+ IConfigurationRoot config,
+ string expected)
+ {
+ string RemoveLineEnds(string source) => source.Replace("\n", "").Replace("\r", "");
+
+ var actual = config.GetDebugView();
+
+ Assert.Equal(
+ RemoveLineEnds(expected),
+ RemoveLineEnds(actual));
+ }
+
+ protected virtual void AssertConfig(
+ IConfigurationRoot config,
+ bool expectNulls = false,
+ string nullValue = null)
+ {
+ var value1 = expectNulls ? nullValue : "Value1";
+ var value12 = expectNulls ? nullValue : "Value12";
+ var value123 = expectNulls ? nullValue : "Value123";
+ var arrayvalue0 = expectNulls ? nullValue : "ArrayValue0";
+ var arrayvalue1 = expectNulls ? nullValue : "ArrayValue1";
+ var arrayvalue2 = expectNulls ? nullValue : "ArrayValue2";
+ var value344 = expectNulls ? nullValue : "Value344";
+
+ Assert.Equal(value1, config["Key1"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(value12, config["Section1:Key2"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(value123, config["Section1:Section2:Key3"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue0, config["Section1:Section2:Key3a:0"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue1, config["Section1:Section2:Key3a:1"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue2, config["Section1:Section2:Key3a:2"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(value344, config["Section3:Section4:Key4"], StringComparer.InvariantCultureIgnoreCase);
+
+ var section1 = config.GetSection("Section1");
+ Assert.Equal(value12, section1["Key2"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(value123, section1["Section2:Key3"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue0, section1["Section2:Key3a:0"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue1, section1["Section2:Key3a:1"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue2, section1["Section2:Key3a:2"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1", section1.Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(section1.Value);
+
+ var section2 = config.GetSection("Section1:Section2");
+ Assert.Equal(value123, section2["Key3"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue0, section2["Key3a:0"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue1, section2["Key3a:1"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue2, section2["Key3a:2"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Section2", section2.Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(section2.Value);
+
+ section2 = section1.GetSection("Section2");
+ Assert.Equal(value123, section2["Key3"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue0, section2["Key3a:0"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue1, section2["Key3a:1"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue2, section2["Key3a:2"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Section2", section2.Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(section2.Value);
+
+ var section3a = section2.GetSection("Key3a");
+ Assert.Equal(arrayvalue0, section3a["0"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue1, section3a["1"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue2, section3a["2"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Section2:Key3a", section3a.Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(section3a.Value);
+
+ var section3 = config.GetSection("Section3");
+ Assert.Equal("Section3", section3.Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(section3.Value);
+
+ var section4 = config.GetSection("Section3:Section4");
+ Assert.Equal(value344, section4["Key4"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section3:Section4", section4.Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(section4.Value);
+
+ section4 = config.GetSection("Section3").GetSection("Section4");
+ Assert.Equal(value344, section4["Key4"], StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section3:Section4", section4.Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(section4.Value);
+
+ var sections = config.GetChildren().ToList();
+
+ Assert.Equal(3, sections.Count);
+
+ Assert.Equal("Key1", sections[0].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Key1", sections[0].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(value1, sections[0].Value, StringComparer.InvariantCultureIgnoreCase);
+
+ Assert.Equal("Section1", sections[1].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1", sections[1].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(sections[1].Value);
+
+ Assert.Equal("Section3", sections[2].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section3", sections[2].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(sections[2].Value);
+
+ sections = section1.GetChildren().ToList();
+
+ Assert.Equal(2, sections.Count);
+
+ Assert.Equal("Key2", sections[0].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Key2", sections[0].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(value12, sections[0].Value, StringComparer.InvariantCultureIgnoreCase);
+
+ Assert.Equal("Section2", sections[1].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Section2", sections[1].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(sections[1].Value);
+
+ sections = section2.GetChildren().ToList();
+
+ Assert.Equal(2, sections.Count);
+
+ Assert.Equal("Key3", sections[0].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Section2:Key3", sections[0].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(value123, sections[0].Value, StringComparer.InvariantCultureIgnoreCase);
+
+ Assert.Equal("Key3a", sections[1].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Section2:Key3a", sections[1].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(sections[1].Value);
+
+ sections = section3a.GetChildren().ToList();
+
+ Assert.Equal(3, sections.Count);
+
+ Assert.Equal("0", sections[0].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Section2:Key3a:0", sections[0].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue0, sections[0].Value, StringComparer.InvariantCultureIgnoreCase);
+
+ Assert.Equal("1", sections[1].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Section2:Key3a:1", sections[1].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue1, sections[1].Value, StringComparer.InvariantCultureIgnoreCase);
+
+ Assert.Equal("2", sections[2].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1:Section2:Key3a:2", sections[2].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(arrayvalue2, sections[2].Value, StringComparer.InvariantCultureIgnoreCase);
+
+ sections = section3.GetChildren().ToList();
+
+ Assert.Single(sections);
+
+ Assert.Equal("Section4", sections[0].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section3:Section4", sections[0].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(sections[0].Value);
+
+ sections = section4.GetChildren().ToList();
+
+ Assert.Single(sections);
+
+ Assert.Equal("Key4", sections[0].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section3:Section4:Key4", sections[0].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal(value344, sections[0].Value, StringComparer.InvariantCultureIgnoreCase);
+ }
+
+ protected abstract (IConfigurationProvider Provider, Action Initializer) LoadThroughProvider(TestSection testConfig);
+
+ protected virtual IConfigurationRoot BuildConfigRoot(
+ params (IConfigurationProvider Provider, Action Initializer)[] providers)
+ {
+ var root = new ConfigurationRoot(providers.Select(e => e.Provider).ToList());
+
+ foreach (var initializer in providers.Select(e => e.Initializer))
+ {
+ initializer();
+ }
+
+ return root;
+ }
+
+ protected static (IConfigurationProvider Provider, Action Initializer) LoadUsingMemoryProvider(TestSection testConfig)
+ {
+ var values = new List>();
+ SectionToValues(testConfig, "", values);
+
+ return (new MemoryConfigurationProvider(
+ new MemoryConfigurationSource
+ {
+ InitialData = values
+ }),
+ () => { });
+ }
+
+ protected static void SectionToValues(
+ TestSection section,
+ string sectionName,
+ IList> values)
+ {
+ foreach (var tuple in section.Values.SelectMany(e => e.Value.Expand(e.Key)))
+ {
+ values.Add(new KeyValuePair(sectionName + tuple.Key, tuple.Value));
+ }
+
+ foreach (var tuple in section.Sections)
+ {
+ SectionToValues(
+ tuple.Section,
+ sectionName + tuple.Key + ":",
+ values);
+ }
+ }
+
+ protected class TestKeyValue
+ {
+ public object Value { get; }
+
+ public TestKeyValue(string value)
+ {
+ Value = value;
+ }
+
+ public TestKeyValue(string[] values)
+ {
+ Value = values;
+ }
+
+ public static implicit operator TestKeyValue(string value) => new TestKeyValue(value);
+ public static implicit operator TestKeyValue(string[] values) => new TestKeyValue(values);
+
+ public string[] AsArray => Value as string[];
+
+ public string AsString => Value as string;
+
+ public IEnumerable<(string Key, string Value)> Expand(string key)
+ {
+ if (AsArray == null)
+ {
+ yield return (key, AsString);
+ }
+ else
+ {
+ for (var i = 0; i < AsArray.Length; i++)
+ {
+ yield return ($"{key}:{i}", AsArray[i]);
+ }
+ }
+ }
+ }
+
+ protected class TestSection
+ {
+ public IEnumerable<(string Key, TestKeyValue Value)> Values { get; set; }
+ = Enumerable.Empty<(string, TestKeyValue)>();
+
+ public IEnumerable<(string Key, TestSection Section)> Sections { get; set; }
+ = Enumerable.Empty<(string, TestSection)>();
+
+ public static TestSection TestConfig { get; }
+ = new TestSection
+ {
+ Values = new[] { ("Key1", (TestKeyValue)"Value1") },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"Value344")}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection NoValuesTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[] { ("Key1", (TestKeyValue)"------") },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"-------")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"-----"),
+ ("Key3a", (TestKeyValue)new[] {"-----------", "-----------", "-----------"})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"--------")}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection MissingSection2ValuesConfig { get; }
+ = new TestSection
+ {
+ Values = new[] { ("Key1", (TestKeyValue)"Value1") },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0"})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"Value344")}
+ })
+ }
+ })
+ }
+ };
+
+
+ public static TestSection MissingSection4Config { get; }
+ = new TestSection
+ {
+ Values = new[] { ("Key1", (TestKeyValue)"Value1") },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection())
+ }
+ };
+
+ public static TestSection DifferentCasedTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[] { ("KeY1", (TestKeyValue)"Value1") },
+ Sections = new[]
+ {
+ ("SectioN1", new TestSection
+ {
+ Values = new[] {("KeY2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("SectioN2", new TestSection
+ {
+ Values = new[]
+ {
+ ("KeY3", (TestKeyValue)"Value123"),
+ ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+ }
+ }),
+ ("SectioN3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("SectioN4", new TestSection
+ {
+ Values = new[] {("KeY4", (TestKeyValue)"Value344")}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection DuplicatesTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[]
+ {
+ ("Key1", (TestKeyValue)"Value1"),
+ ("Key1", (TestKeyValue)"Value1")
+ },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ }),
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"Value344")}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection DuplicatesDifferentCaseTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[]
+ {
+ ("Key1", (TestKeyValue)"Value1"),
+ ("KeY1", (TestKeyValue)"Value1")
+ },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ }),
+ ("SectioN2", new TestSection
+ {
+ Values = new[]
+ {
+ ("KeY3", (TestKeyValue)"Value123"),
+ ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"Value344")}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection NullsTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[] { ("Key1", new TestKeyValue((string)null)) },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", new TestKeyValue((string)null))},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", new TestKeyValue((string)null)),
+ ("Key3a", (TestKeyValue)new string[] {null, null, null})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", new TestKeyValue((string)null))}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection ExtraValuesTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[]
+ {
+ ("Key1", (TestKeyValue)"Value1"),
+ ("Key1r", (TestKeyValue)"Value1r")
+ },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key2", (TestKeyValue)"Value12"),
+ ("Key2r", (TestKeyValue)"Value12r")
+ },
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2", "ArrayValue2r"}),
+ ("Key3ar", (TestKeyValue)new[] {"ArrayValue0r"})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"Value344")}
+ })
+ }
+ }),
+ ("Section5r", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section6r", new TestSection
+ {
+ Values = new[] {("Key5r", (TestKeyValue)"Value565r")}
+ })
+ }
+ })
+ }
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs b/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs
new file mode 100644
index 000000000000..794f7abf23fc
--- /dev/null
+++ b/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs
@@ -0,0 +1,416 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
+{
+ public class KeyPerFileTests
+ {
+ [Fact]
+ public void DoesNotThrowWhenOptionalAndNoSecrets()
+ {
+ new ConfigurationBuilder().AddKeyPerFile(o => o.Optional = true).Build();
+ }
+
+ [Fact]
+ public void DoesNotThrowWhenOptionalAndDirectoryDoesntExist()
+ {
+ new ConfigurationBuilder().AddKeyPerFile("nonexistent", true).Build();
+ }
+
+ [Fact]
+ public void ThrowsWhenNotOptionalAndDirectoryDoesntExist()
+ {
+ var e = Assert.Throws(() => new ConfigurationBuilder().AddKeyPerFile("nonexistent", false).Build());
+ Assert.Contains("The path must be absolute.", e.Message);
+ }
+
+ [Fact]
+ public void CanLoadMultipleSecrets()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanLoadMultipleSecretsWithDirectory()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"),
+ new TestFile("directory"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanLoadNestedKeys()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret0__Secret1__Secret2__Key", "SecretValue2"),
+ new TestFile("Secret0__Secret1__Key", "SecretValue1"),
+ new TestFile("Secret0__Key", "SecretValue0"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ Assert.Equal("SecretValue0", config["Secret0:Key"]);
+ Assert.Equal("SecretValue1", config["Secret0:Secret1:Key"]);
+ Assert.Equal("SecretValue2", config["Secret0:Secret1:Secret2:Key"]);
+ }
+
+ [Fact]
+ public void CanIgnoreFilesWithDefault()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("ignore.Secret0", "SecretValue0"),
+ new TestFile("ignore.Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ Assert.Null(config["ignore.Secret0"]);
+ Assert.Null(config["ignore.Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanTurnOffDefaultIgnorePrefixWithCondition()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("ignore.Secret0", "SecretValue0"),
+ new TestFile("ignore.Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.IgnoreCondition = null;
+ })
+ .Build();
+
+ Assert.Equal("SecretValue0", config["ignore.Secret0"]);
+ Assert.Equal("SecretValue1", config["ignore.Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanIgnoreAllWithCondition()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret0", "SecretValue0"),
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.IgnoreCondition = s => true;
+ })
+ .Build();
+
+ Assert.Empty(config.AsEnumerable());
+ }
+
+ [Fact]
+ public void CanIgnoreFilesWithCustomIgnore()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("meSecret0", "SecretValue0"),
+ new TestFile("meSecret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.IgnorePrefix = "me";
+ })
+ .Build();
+
+ Assert.Null(config["meSecret0"]);
+ Assert.Null(config["meSecret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanUnIgnoreDefaultFiles()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("ignore.Secret0", "SecretValue0"),
+ new TestFile("ignore.Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.IgnorePrefix = null;
+ })
+ .Build();
+
+ Assert.Equal("SecretValue0", config["ignore.Secret0"]);
+ Assert.Equal("SecretValue1", config["ignore.Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void BindingDoesNotThrowIfReloadedDuringBinding()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Number", "-2"),
+ new TestFile("Text", "Foo"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ MyOptions options = null;
+
+ using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
+ {
+ void ReloadLoop()
+ {
+ while (!cts.IsCancellationRequested)
+ {
+ config.Reload();
+ }
+ }
+
+ _ = Task.Run(ReloadLoop);
+
+ while (!cts.IsCancellationRequested)
+ {
+ options = config.Get();
+ }
+ }
+
+ Assert.Equal(-2, options.Number);
+ Assert.Equal("Foo", options.Text);
+ }
+
+ [Fact]
+ public void ReloadConfigWhenReloadOnChangeIsTrue()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.ReloadOnChange = true;
+ }).Build();
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+
+ testFileProvider.ChangeFiles(
+ new TestFile("Secret1", "NewSecretValue1"),
+ new TestFile("Secret3", "NewSecretValue3"));
+
+ Assert.Equal("NewSecretValue1", config["Secret1"]);
+ Assert.Null(config["NewSecret2"]);
+ Assert.Equal("NewSecretValue3", config["Secret3"]);
+ }
+
+ [Fact]
+ public void SameConfigWhenReloadOnChangeIsFalse()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.ReloadOnChange = false;
+ }).Build();
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+
+ testFileProvider.ChangeFiles(
+ new TestFile("Secret1", "NewSecretValue1"),
+ new TestFile("Secret3", "NewSecretValue3"));
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void NoFilesReloadWhenAddedFiles()
+ {
+ var testFileProvider = new TestFileProvider();
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.ReloadOnChange = true;
+ }).Build();
+
+ Assert.Empty(config.AsEnumerable());
+
+ testFileProvider.ChangeFiles(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ private sealed class MyOptions
+ {
+ public int Number { get; set; }
+ public string Text { get; set; }
+ }
+ }
+
+ class TestFileProvider : IFileProvider
+ {
+ IDirectoryContents _contents;
+ MockChangeToken _changeToken;
+
+ public TestFileProvider(params IFileInfo[] files)
+ {
+ _contents = new TestDirectoryContents(files);
+ _changeToken = new MockChangeToken();
+ }
+
+ public IDirectoryContents GetDirectoryContents(string subpath) => _contents;
+
+ public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory");
+
+ public IChangeToken Watch(string filter) => _changeToken;
+
+ internal void ChangeFiles(params IFileInfo[] files)
+ {
+ _contents = new TestDirectoryContents(files);
+ _changeToken.RaiseCallback();
+ }
+ }
+
+ class MockChangeToken : IChangeToken
+ {
+ private Action _callback;
+
+ public bool ActiveChangeCallbacks => true;
+
+ public bool HasChanged => true;
+
+ public IDisposable RegisterChangeCallback(Action