diff --git a/src/DefaultBuilder/src/ConfigurationProviderSource.cs b/src/DefaultBuilder/src/ConfigurationProviderSource.cs new file mode 100644 index 000000000000..b22657cc3e8b --- /dev/null +++ b/src/DefaultBuilder/src/ConfigurationProviderSource.cs @@ -0,0 +1,99 @@ +// 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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Builder +{ + internal sealed class ConfigurationProviderSource : IConfigurationSource + { + private readonly IConfigurationProvider _configurationProvider; + + public ConfigurationProviderSource(IConfigurationProvider configurationProvider) + { + _configurationProvider = configurationProvider; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new IgnoreFirstLoadConfigurationProvider(_configurationProvider); + } + + // These providers have already been loaded, so no need to reload initially. + // Otherwise, providers that cannot be reloaded like StreamConfigurationProviders will fail. + private sealed class IgnoreFirstLoadConfigurationProvider : IConfigurationProvider, IEnumerable, IDisposable + { + private readonly IConfigurationProvider _provider; + + private bool _hasIgnoredFirstLoad; + + public IgnoreFirstLoadConfigurationProvider(IConfigurationProvider provider) + { + _provider = provider; + } + + public IEnumerable GetChildKeys(IEnumerable earlierKeys, string parentPath) + { + return _provider.GetChildKeys(earlierKeys, parentPath); + } + + public IChangeToken GetReloadToken() + { + return _provider.GetReloadToken(); + } + + public void Load() + { + if (!_hasIgnoredFirstLoad) + { + _hasIgnoredFirstLoad = true; + return; + } + + _provider.Load(); + } + + public void Set(string key, string value) + { + _provider.Set(key, value); + } + + public bool TryGet(string key, out string value) + { + return _provider.TryGet(key, out value); + } + + // Provide access to the original IConfigurationProvider via a single-element IEnumerable to code that goes out of its way to look for it. + public IEnumerator GetEnumerator() => GetUnwrappedEnumerable().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetUnwrappedEnumerable().GetEnumerator(); + + public override bool Equals(object? obj) + { + return _provider.Equals(obj); + } + + public override int GetHashCode() + { + return _provider.GetHashCode(); + } + + public override string? ToString() + { + return _provider.ToString(); + } + + public void Dispose() + { + (_provider as IDisposable)?.Dispose(); + } + + private IEnumerable GetUnwrappedEnumerable() + { + yield return _provider; + } + } + } +} diff --git a/src/DefaultBuilder/src/TrackingChainedConfigurationSource.cs b/src/DefaultBuilder/src/TrackingChainedConfigurationSource.cs new file mode 100644 index 000000000000..ae31605a6b0e --- /dev/null +++ b/src/DefaultBuilder/src/TrackingChainedConfigurationSource.cs @@ -0,0 +1,25 @@ +// 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; + +namespace Microsoft.AspNetCore.Builder +{ + internal sealed class TrackingChainedConfigurationSource : IConfigurationSource + { + private readonly ChainedConfigurationSource _chainedConfigurationSource = new(); + + public TrackingChainedConfigurationSource(ConfigurationManager configManager) + { + _chainedConfigurationSource.Configuration = configManager; + } + + public IConfigurationProvider? BuiltProvider { get; set; } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + BuiltProvider = _chainedConfigurationSource.Build(builder); + return BuiltProvider; + } + } +} diff --git a/src/DefaultBuilder/src/WebApplication.cs b/src/DefaultBuilder/src/WebApplication.cs index d6c8ca5c609a..9d0a837c8300 100644 --- a/src/DefaultBuilder/src/WebApplication.cs +++ b/src/DefaultBuilder/src/WebApplication.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -20,11 +19,11 @@ namespace Microsoft.AspNetCore.Builder /// public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable { + internal const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; + private readonly IHost _host; private readonly List _dataSources = new(); - internal static string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; - internal WebApplication(IHost host) { _host = host; diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index e7e6269d0205..19e8c0661c84 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Linq; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -15,11 +16,12 @@ namespace Microsoft.AspNetCore.Builder /// public sealed class WebApplicationBuilder { + private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder"; + private readonly HostBuilder _hostBuilder = new(); private readonly BootstrapHostBuilder _bootstrapHostBuilder; private readonly WebApplicationServiceCollection _services = new(); private readonly List> _hostConfigurationValues; - private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder"; private WebApplication? _builtApplication; @@ -62,7 +64,6 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action { if (args is { Length: > 0 }) @@ -74,7 +75,6 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action(Configuration); + Services.AddSingleton(_ => Configuration); } /// @@ -148,14 +148,13 @@ public WebApplication Build() builder.AddInMemoryCollection(_hostConfigurationValues); }); + var chainedConfigSource = new TrackingChainedConfigurationSource(Configuration); + // Wire up the application configuration by copying the already built configuration providers over to final configuration builder. // We wrap the existing provider in a configuration source to avoid re-bulding the already added configuration sources. _hostBuilder.ConfigureAppConfiguration(builder => { - foreach (var provider in ((IConfigurationRoot)Configuration).Providers) - { - builder.Sources.Add(new ConfigurationProviderSource(provider)); - } + builder.Add(chainedConfigSource); foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties) { @@ -173,17 +172,6 @@ public WebApplication Build() // we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder. foreach (var s in _services) { - // Skip the configuration manager instance we added earlier - // we're already going to wire it up to this new configuration source - // after we've built the application. There's a chance the user manually added - // this as well but we still need to remove it from the final configuration - // to avoid cycles in the configuration graph - if (s.ServiceType == typeof(IConfiguration) && - s.ImplementationInstance == Configuration) - { - continue; - } - services.Add(s); } @@ -205,6 +193,25 @@ public WebApplication Build() // Drop the reference to the existing collection and set the inner collection // to the new one. This allows code that has references to the service collection to still function. _services.InnerCollection = services; + + var hostBuilderProviders = ((IConfigurationRoot)context.Configuration).Providers; + + if (!hostBuilderProviders.Contains(chainedConfigSource.BuiltProvider)) + { + // Something removed the _hostBuilder's TrackingChainedConfigurationSource pointing back to the ConfigurationManager. + // This is likely a test using WebApplicationFactory. Replicate the effect by clearing the ConfingurationManager sources. + ((IConfigurationBuilder)Configuration).Sources.Clear(); + } + + // Make builder.Configuration match the final configuration. To do that, we add the additional + // providers in the inner _hostBuilders's Configuration to the ConfigurationManager. + foreach (var provider in hostBuilderProviders) + { + if (!ReferenceEquals(provider, chainedConfigSource.BuiltProvider)) + { + ((IConfigurationBuilder)Configuration).Add(new ConfigurationProviderSource(provider)); + } + } }); // Run the other callbacks on the final host builder @@ -212,14 +219,13 @@ public WebApplication Build() _builtApplication = new WebApplication(_hostBuilder.Build()); - // Make builder.Configuration match the final configuration. To do that - // we clear the sources and add the built configuration as a source - ((IConfigurationBuilder)Configuration).Sources.Clear(); - Configuration.AddConfiguration(_builtApplication.Configuration); - // Mark the service collection as read-only to prevent future modifications _services.IsReadOnly = true; + // Resolve both the _hostBuilder's Configuration and builder.Configuration to mark both as resolved within the + // service provider ensuring both will be properly disposed with the provider. + _ = _builtApplication.Services.GetService>(); + return _builtApplication; } @@ -300,20 +306,5 @@ public LoggingBuilder(IServiceCollection services) public IServiceCollection Services { get; } } - - private sealed class ConfigurationProviderSource : IConfigurationSource - { - private readonly IConfigurationProvider _configurationProvider; - - public ConfigurationProviderSource(IConfigurationProvider configurationProvider) - { - _configurationProvider = configurationProvider; - } - - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return _configurationProvider; - } - } } } diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs index c86e98459ba8..f0c1d9a700f6 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.IO; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; -using Xunit; +using Microsoft.Extensions.Logging.EventLog; namespace Microsoft.AspNetCore.Tests { @@ -15,9 +13,12 @@ public class WebApplicationFunctionalTests : LoggedTest [Fact] public async Task LoggingConfigurationSectionPassedToLoggerByDefault() { + var contentRootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(contentRootPath); + try { - await File.WriteAllTextAsync("appsettings.json", @" + await File.WriteAllTextAsync(Path.Combine(contentRootPath, "appsettings.json"), @" { ""Logging"": { ""LogLevel"": { @@ -26,7 +27,7 @@ await File.WriteAllTextAsync("appsettings.json", @" } }"); - await using var app = WebApplication.Create(); + await using var app = WebApplication.Create(new[] { "--contentRoot", contentRootPath }); var factory = (ILoggerFactory)app.Services.GetService(typeof(ILoggerFactory)); var logger = factory.CreateLogger("Test"); @@ -48,16 +49,19 @@ await File.WriteAllTextAsync("appsettings.json", @" } finally { - File.Delete("appsettings.json"); + Directory.Delete(contentRootPath, recursive: true); } } [Fact] public async Task EnvironmentSpecificLoggingConfigurationSectionPassedToLoggerByDefault() { + var contentRootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(contentRootPath); + try { - await File.WriteAllTextAsync("appsettings.Development.json", @" + await File.WriteAllTextAsync(Path.Combine(contentRootPath, "appsettings.Development.json"), @" { ""Logging"": { ""LogLevel"": { @@ -66,13 +70,7 @@ await File.WriteAllTextAsync("appsettings.Development.json", @" } }"); - var app = WebApplication.Create(new[] { "--environment", "Development" }); - - // TODO: Make this work! I think it should be possible if we register our Configuration - // as a ChainedConfigurationSource instead of copying over the bootstrapped IConfigurationSources. - //var builder = WebApplication.CreateBuilder(); - //builder.Environment.EnvironmentName = "Development"; - //await using var app = builder.Build(); + var app = WebApplication.Create(new[] { "--environment", "Development", "--contentRoot", contentRootPath }); var factory = (ILoggerFactory)app.Services.GetService(typeof(ILoggerFactory)); var logger = factory.CreateLogger("Test"); @@ -94,9 +92,86 @@ await File.WriteAllTextAsync("appsettings.Development.json", @" } finally { - File.Delete("appsettings.json"); + Directory.Delete(contentRootPath, recursive: true); } } + [Fact] + public async Task LoggingConfigurationReactsToRuntimeChanges() + { + var contentRootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(contentRootPath); + + try + { + await File.WriteAllTextAsync(Path.Combine(contentRootPath, "appsettings.json"), @" +{ + ""Logging"": { + ""LogLevel"": { + ""Default"": ""Error"" + } + } +}"); + + var builder = WebApplication.CreateBuilder(new WebApplicationOptions + { + ContentRootPath = contentRootPath, + }); + + // Disable the EventLogLoggerProvider because HostBuilder.ConfigureDefaults() configures it to log everything warning and higher which overrides non-provider-specific config. + // https://github.com/dotnet/runtime/blob/8048fe613933a1cd91e3fad6d571c74f726143ef/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs#L238 + builder.Logging.AddFilter(_ => false); + + await using var app = builder.Build(); + + var factory = (ILoggerFactory)app.Services.GetService(typeof(ILoggerFactory)); + var logger = factory.CreateLogger("Test"); + + Assert.False(logger.IsEnabled(LogLevel.Warning)); + + logger.Log(LogLevel.Warning, 0, "Message", null, (s, e) => + { + Assert.True(false); + return string.Empty; + }); + + // Lower log level from Error to Warning and wait for logging to react to the config changes. + var configChangedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var registration = app.Configuration.GetReloadToken().RegisterChangeCallback( + tcs => ((TaskCompletionSource)tcs).SetResult(), configChangedTcs); + + await File.WriteAllTextAsync(Path.Combine(contentRootPath, "appsettings.json"), @" +{ + ""Logging"": { + ""LogLevel"": { + ""Default"": ""Warning"" + } + } +}"); + + // Wait for a config change notification because logging will not react until this is fired. Even then, it won't react immediately + // so we loop until success or a timeout. + await configChangedTcs.Task.DefaultTimeout(); + + var timeoutTicks = Environment.TickCount64 + Testing.TaskExtensions.DefaultTimeoutDuration; + var logWritten = false; + + while (!logWritten && Environment.TickCount < timeoutTicks) + { + logger.Log(LogLevel.Warning, 0, "Message", null, (s, e) => + { + logWritten = true; + return string.Empty; + }); + } + + Assert.True(logWritten); + Assert.True(logger.IsEnabled(LogLevel.Warning)); + } + finally + { + Directory.Delete(contentRootPath, recursive: true); + } + } } } diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs index 8de225c3a8fd..ddf82c6d53da 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs @@ -6,6 +6,7 @@ using System.Diagnostics.Tracing; using System.Net; using System.Reflection; +using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HostFiltering; using Microsoft.AspNetCore.Hosting; @@ -23,7 +24,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Xunit; [assembly: HostingStartup(typeof(WebApplicationTests.TestHostingStartup))] @@ -700,6 +700,128 @@ public async Task WebApplicationCanObserveConfigurationChangesMadeInBuild() Assert.Equal("F", builder.Configuration["F"]); } + [Fact] + public async Task WebApplicationCanObserveSourcesClearedInBuild() + { + // This mimics what WebApplicationFactory does and runs configure + // services callbacks + using var listener = new HostingListener(hostBuilder => + { + hostBuilder.ConfigureHostConfiguration(config => + { + // Clearing here would not remove the app config added via builder.Configuration. + config.AddInMemoryCollection(new Dictionary() + { + { "A", "A" }, + }); + }); + + hostBuilder.ConfigureAppConfiguration(config => + { + // This clears both the chained host configuration and chained builder.Configuration. + config.Sources.Clear(); + config.AddInMemoryCollection(new Dictionary() + { + { "B", "B" }, + }); + }); + }); + + var builder = WebApplication.CreateBuilder(); + + builder.Configuration.AddInMemoryCollection(new Dictionary() + { + { "C", "C" }, + }); + + await using var app = builder.Build(); + + Assert.True(string.IsNullOrEmpty(app.Configuration["A"])); + Assert.True(string.IsNullOrEmpty(app.Configuration["C"])); + + Assert.Equal("B", app.Configuration["B"]); + + Assert.Same(builder.Configuration, app.Configuration); + } + + [Fact] + public async Task WebApplicationCanHandleStreamBackedConfigurationAddedInBuild() + { + static Stream CreateStreamFromString(string data) => new MemoryStream(Encoding.UTF8.GetBytes(data)); + + using var jsonAStream = CreateStreamFromString(@"{ ""A"": ""A"" }"); + using var jsonBStream = CreateStreamFromString(@"{ ""B"": ""B"" }"); + + // This mimics what WebApplicationFactory does and runs configure + // services callbacks + using var listener = new HostingListener(hostBuilder => + { + hostBuilder.ConfigureHostConfiguration(config => config.AddJsonStream(jsonAStream)); + hostBuilder.ConfigureAppConfiguration(config => config.AddJsonStream(jsonBStream)); + }); + + var builder = WebApplication.CreateBuilder(); + await using var app = builder.Build(); + + Assert.Equal("A", app.Configuration["A"]); + Assert.Equal("B", app.Configuration["B"]); + + Assert.Same(builder.Configuration, app.Configuration); + } + + [Fact] + public async Task WebApplicationDisposesConfigurationProvidersAddedInBuild() + { + var hostConfigSource = new RandomConfigurationSource(); + var appConfigSource = new RandomConfigurationSource(); + + // This mimics what WebApplicationFactory does and runs configure + // services callbacks + using var listener = new HostingListener(hostBuilder => + { + hostBuilder.ConfigureHostConfiguration(config => config.Add(hostConfigSource)); + hostBuilder.ConfigureAppConfiguration(config => config.Add(appConfigSource)); + }); + + var builder = WebApplication.CreateBuilder(); + + { + await using var app = builder.Build(); + + Assert.Equal(1, hostConfigSource.ProvidersBuilt); + Assert.Equal(1, appConfigSource.ProvidersBuilt); + Assert.Equal(1, hostConfigSource.ProvidersLoaded); + Assert.Equal(1, appConfigSource.ProvidersLoaded); + Assert.Equal(0, hostConfigSource.ProvidersDisposed); + Assert.Equal(0, appConfigSource.ProvidersDisposed); + } + + Assert.Equal(1, hostConfigSource.ProvidersBuilt); + Assert.Equal(1, appConfigSource.ProvidersBuilt); + Assert.Equal(1, hostConfigSource.ProvidersLoaded); + Assert.Equal(1, appConfigSource.ProvidersLoaded); + Assert.True(hostConfigSource.ProvidersDisposed > 0); + Assert.True(appConfigSource.ProvidersDisposed > 0); + } + + [Fact] + public async Task WebApplicationMakesOriginalConfigurationProvidersAddedInBuildAccessable() + { + // This mimics what WebApplicationFactory does and runs configure + // services callbacks + using var listener = new HostingListener(hostBuilder => + { + hostBuilder.ConfigureAppConfiguration(config => config.Add(new RandomConfigurationSource())); + }); + + var builder = WebApplication.CreateBuilder(); + await using var app = builder.Build(); + + var wrappedProviders = ((IConfigurationRoot)app.Configuration).Providers.OfType>(); + var unwrappedProviders = wrappedProviders.Select(p => Assert.Single(p)); + Assert.Single(unwrappedProviders.OfType()); + } + [Fact] public void WebApplicationBuilderHostProperties_IsCaseSensitive() { @@ -753,8 +875,7 @@ public void CanResolveIConfigurationBeforeBuildingApplication() var app = builder.Build(); - // These are different - Assert.NotSame(app.Configuration, builder.Configuration); + Assert.Same(app.Configuration, builder.Configuration); } [Fact] @@ -770,8 +891,28 @@ public void ManuallyAddingConfigurationAsServiceWorks() var app = builder.Build(); - // These are different - Assert.NotSame(app.Configuration, builder.Configuration); + Assert.Same(app.Configuration, builder.Configuration); + } + + [Fact] + public void AddingMemoryStreamBackedConfigurationWorks() + { + var builder = WebApplication.CreateBuilder(); + + var jsonConfig = @"{ ""foo"": ""bar"" }"; + using var ms = new MemoryStream(); + using var sw = new StreamWriter(ms); + sw.WriteLine(jsonConfig); + sw.Flush(); + + ms.Position = 0; + builder.Configuration.AddJsonStream(ms); + + Assert.Equal("bar", builder.Configuration["foo"]); + + var app = builder.Build(); + + Assert.Equal("bar", app.Configuration["foo"]); } [Fact] @@ -1512,6 +1653,22 @@ public void ClearingConfigurationDoesNotAffectHostConfiguration() Assert.Equal(Path.GetTempPath(), hostEnv.ContentRootPath); } + [Fact] + public void ConfigurationGetDebugViewWorks() + { + var builder = WebApplication.CreateBuilder(); + + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["foo"] = "bar", + }); + + var app = builder.Build(); + + // Make sure we don't lose "MemoryConfigurationProvider" from GetDebugView() when wrapping the provider. + Assert.Contains("foo=bar (MemoryConfigurationProvider)", ((IConfigurationRoot)app.Configuration).GetDebugView()); + } + [Fact] public void ConfigurationCanBeReloaded() { @@ -1541,23 +1698,77 @@ public void ConfigurationSourcesAreBuiltOnce() Assert.Equal(1, configSource.ProvidersBuilt); } + [Fact] + public void ConfigurationProvidersAreLoadedOnceAfterBuild() + { + var builder = WebApplication.CreateBuilder(); + + var configSource = new RandomConfigurationSource(); + ((IConfigurationBuilder)builder.Configuration).Sources.Add(configSource); + + using var app = builder.Build(); + + Assert.Equal(1, configSource.ProvidersLoaded); + } + + [Fact] + public void ConfigurationProvidersAreDisposedWithWebApplication() + { + var builder = WebApplication.CreateBuilder(); + + var configSource = new RandomConfigurationSource(); + ((IConfigurationBuilder)builder.Configuration).Sources.Add(configSource); + + { + using var app = builder.Build(); + + Assert.Equal(0, configSource.ProvidersDisposed); + } + + Assert.Equal(1, configSource.ProvidersDisposed); + } + + [Fact] + public void ConfigurationProviderTypesArePreserved() + { + var builder = WebApplication.CreateBuilder(); + + ((IConfigurationBuilder)builder.Configuration).Sources.Add(new RandomConfigurationSource()); + + var app = builder.Build(); + + Assert.Single(((IConfigurationRoot)app.Configuration).Providers.OfType()); + } + public class RandomConfigurationSource : IConfigurationSource { public int ProvidersBuilt { get; set; } + public int ProvidersLoaded { get; set; } + public int ProvidersDisposed { get; set; } public IConfigurationProvider Build(IConfigurationBuilder builder) { ProvidersBuilt++; - return new RandomConfigurationProvider(); + return new RandomConfigurationProvider(this); } } - public class RandomConfigurationProvider : ConfigurationProvider + public class RandomConfigurationProvider : ConfigurationProvider, IDisposable { + private readonly RandomConfigurationSource _source; + + public RandomConfigurationProvider(RandomConfigurationSource source) + { + _source = source; + } + public override void Load() { + _source.ProvidersLoaded++; Data["Random"] = Guid.NewGuid().ToString(); } + + public void Dispose() => _source.ProvidersDisposed++; } public class TestHostingStartup : IHostingStartup diff --git a/src/Shared/TaskExtensions.cs b/src/Shared/TaskExtensions.cs index 78c94075acb5..fbedf3217fcb 100644 --- a/src/Shared/TaskExtensions.cs +++ b/src/Shared/TaskExtensions.cs @@ -25,9 +25,9 @@ static class TaskExtensions #if DEBUG // Shorter duration when running tests with debug. // Less time waiting for hang unit tests to fail in aspnetcore solution. - private const int DefaultTimeoutDuration = 5 * 1000; + public const int DefaultTimeoutDuration = 5 * 1000; #else - private const int DefaultTimeoutDuration = 30 * 1000; + public const int DefaultTimeoutDuration = 30 * 1000; #endif public static TimeSpan DefaultTimeoutTimeSpan { get; } = TimeSpan.FromMilliseconds(DefaultTimeoutDuration);