File: DeferredHostBuilder.cs
Web Access
Project: src\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);
    }
}