| File: DeferredHostBuilder.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.Testing\src\Microsoft.AspNetCore.Mvc.Testing.csproj (Microsoft.AspNetCore.Mvc.Testing) |
// 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.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Microsoft.AspNetCore.Mvc.Testing; // This host builder captures calls to the IHostBuilder then replays them in the call to ConfigureHostBuilder internal sealed class DeferredHostBuilder : IHostBuilder { public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>(); private Action<IHostBuilder> _configure; private Func<string[], object>? _hostFactory; private readonly ConfigurationManager _hostConfiguration = new(); // This task represents a call to IHost.Start, we create it here preemptively in case the application // exits due to an exception or because it didn't wait for the shutdown signal private readonly TaskCompletionSource _hostStartTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); public DeferredHostBuilder() { _configure = b => { // Copy the properties from this builder into the builder // that we're going to receive foreach (var pair in Properties) { b.Properties[pair.Key] = pair.Value; } }; } public IHost Build() { // Hosting configuration is being provided by args so that // we can impact WebApplicationBuilder based applications. var args = new List<string>(); // Transform the host configuration into command line arguments foreach (var (key, value) in _hostConfiguration.AsEnumerable()) { args.Add($"--{key}={value}"); } // This will never be null if the case where Build is being called var host = (IHost)_hostFactory!(args.ToArray()); // We can't return the host directly since we need to defer the call to StartAsync return new DeferredHost(host, _hostStartTcs); } public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate) { _configure += b => b.ConfigureAppConfiguration(configureDelegate); return this; } public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate) { _configure += b => b.ConfigureContainer(configureDelegate); return this; } public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate) { // Run this immediately so that we can capture the host configuration // before we pass it to the application. We can do this for app configuration // as well if it becomes necessary. configureDelegate(_hostConfiguration); return this; } public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate) { _configure += b => b.ConfigureServices(configureDelegate); return this; } public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) where TContainerBuilder : notnull { _configure += b => b.UseServiceProviderFactory(factory); return this; } public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) where TContainerBuilder : notnull { _configure += b => b.UseServiceProviderFactory(factory); return this; } public void ConfigureHostBuilder(object hostBuilder) { _configure(((IHostBuilder)hostBuilder)); } public void EntryPointCompleted(Exception? exception) { // If the entry point completed we'll set the tcs just in case the application doesn't call IHost.Start/StartAsync. if (exception is not null) { _hostStartTcs.TrySetException(exception); } else { _hostStartTcs.TrySetResult(); } } public void SetHostFactory(Func<string[], object> hostFactory) { _hostFactory = hostFactory; } private sealed class DeferredHost : IHost, IAsyncDisposable { private readonly IHost _host; private readonly TaskCompletionSource _hostStartedTcs; public DeferredHost(IHost host, TaskCompletionSource hostStartedTcs) { _host = host; _hostStartedTcs = hostStartedTcs; } public IServiceProvider Services => _host.Services; public void Dispose() => _host.Dispose(); public async ValueTask DisposeAsync() { if (_host is IAsyncDisposable disposable) { await disposable.DisposeAsync().ConfigureAwait(false); return; } Dispose(); } public async Task StartAsync(CancellationToken cancellationToken = default) { // Wait on the existing host to start running and have this call wait on that. This avoids starting the actual host too early and // leaves the application in charge of calling start. using var reg = cancellationToken.UnsafeRegister(_ => _hostStartedTcs.TrySetCanceled(), null); // REVIEW: This will deadlock if the application creates the host but never calls start. This is mitigated by the cancellationToken // but it's rarely a valid token for Start using var reg2 = _host.Services.GetRequiredService<IHostApplicationLifetime>().ApplicationStarted.UnsafeRegister(_ => _hostStartedTcs.TrySetResult(), null); await _hostStartedTcs.Task.ConfigureAwait(false); } public Task StopAsync(CancellationToken cancellationToken = default) => _host.StopAsync(cancellationToken); } }