File: Internal\Host.cs
Web Access
Project: src\runtime\src\libraries\Microsoft.Extensions.Hosting\src\Microsoft.Extensions.Hosting.csproj (Microsoft.Extensions.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Hosting.Internal
{
    [DebuggerDisplay("{DebuggerToString(),nq}")]
    [DebuggerTypeProxy(typeof(HostDebugView))]
    internal sealed class Host : IHost, IAsyncDisposable
    {
        private readonly ILogger<Host> _logger;
        private readonly IHostLifetime _hostLifetime;
        private readonly ApplicationLifetime _applicationLifetime;
        private readonly HostOptions _options;
        private readonly IHostEnvironment _hostEnvironment;
        private readonly PhysicalFileProvider _defaultProvider;
        private IEnumerable<IHostedService>? _hostedServices;
        private IEnumerable<IHostedLifecycleService>? _hostedLifecycleServices;
        private bool _hostStarting;
        private bool _hostStopped;
        private List<Task>? _backgroundServiceTasks;
        private List<Exception>? _backgroundServiceExceptions;

        public Host(IServiceProvider services,
                    IHostEnvironment hostEnvironment,
                    PhysicalFileProvider defaultProvider,
                    IHostApplicationLifetime applicationLifetime,
                    ILogger<Host> logger,
                    IHostLifetime hostLifetime,
                    IOptions<HostOptions> options)
        {
            ArgumentNullException.ThrowIfNull(services);
            ArgumentNullException.ThrowIfNull(applicationLifetime);
            ArgumentNullException.ThrowIfNull(logger);
            ArgumentNullException.ThrowIfNull(hostLifetime);

            Services = services;
            _applicationLifetime = (applicationLifetime as ApplicationLifetime)!;
            _hostEnvironment = hostEnvironment;
            _defaultProvider = defaultProvider;

            if (_applicationLifetime is null)
            {
                throw new ArgumentException(SR.IHostApplicationLifetimeReplacementNotSupported, nameof(applicationLifetime));
            }
            _logger = logger;
            _hostLifetime = hostLifetime;
            _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
        }

        public IServiceProvider Services { get; }

        /// <summary>
        /// Order:
        ///  IHostLifetime.WaitForStartAsync
        ///  Startup validation: a custom sync-only IStartupValidator (if any) via Validate(), otherwise every IAsyncStartupValidator via ValidateAsync()
        ///  IHostedLifecycleService.StartingAsync
        ///  IHostedService.Start
        ///  IHostedLifecycleService.StartedAsync
        ///  IHostApplicationLifetime.ApplicationStarted
        /// </summary>
        public async Task StartAsync(CancellationToken cancellationToken = default)
        {
            _logger.Starting();

            using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping))
            {
                if (_options.StartupTimeout != Timeout.InfiniteTimeSpan)
                    cts.CancelAfter(_options.StartupTimeout);

                cancellationToken = cts.Token;

                // This may not catch exceptions.
                await _hostLifetime.WaitForStartAsync(cancellationToken).ConfigureAwait(false);
                cancellationToken.ThrowIfCancellationRequested();

                List<Exception> exceptions = new();
                _hostStarting = true;
                bool concurrent = _options.ServicesStartConcurrently;
                bool abortOnFirstException = !concurrent;

                try
                {
                    // Run startup validation before resolving hosted services so that invalid configuration
                    // fails fast and a hosted service reading validated options in its constructor observes
                    // the startup-validated instance.
#pragma warning disable SYSLIB0066 // IStartupValidator is obsolete but retained for compatibility.
                    IStartupValidator? startupValidator = Services.GetService<IStartupValidator>();
#pragma warning restore SYSLIB0066
                    IAsyncStartupValidator[] asyncValidators = Array.Empty<IAsyncStartupValidator>();
                    bool runSyncValidator;

                    // A sync-only IStartupValidator takes precedence without resolving async validators that will
                    // not run. This preserves the legacy replacement behavior and avoids their activation side effects.
                    if (startupValidator is not null and not IAsyncStartupValidator)
                    {
                        runSyncValidator = true;
                    }
                    else
                    {
                        asyncValidators = Services.GetServices<IAsyncStartupValidator>().ToArray();

                        // The built-in validator is intentionally exposed through both contracts as one shared
                        // instance. A legacy service that is also the exact object in the async collection follows the
                        // async path, recognizing that compatibility alias. Otherwise preserve legacy replacement
                        // behavior. Object identity avoids conflating independent registrations of the same type.
                        bool legacyInstanceIsAlsoResolvedAsAsync =
                            startupValidator is not null &&
                            asyncValidators.Any(asyncValidator => ReferenceEquals(asyncValidator, startupValidator));

                        runSyncValidator =
                            startupValidator is not null &&
                            !legacyInstanceIsAlsoResolvedAsAsync;
                    }

                    if (runSyncValidator)
                    {
                        startupValidator?.Validate();
                    }
                    else
                    {
                        // Run every registered async startup validator so multiple IAsyncStartupValidator instances
                        // all participate, aggregating their validation failures.
                        List<Exception>? validationFailures = null;
                        foreach (IAsyncStartupValidator asyncValidator in asyncValidators)
                        {
                            try
                            {
                                await asyncValidator.ValidateAsync(cancellationToken).ConfigureAwait(false);
                            }
                            catch (OptionsValidationException ex)
                            {
                                (validationFailures ??= new()).Add(ex);
                            }
                            catch (AggregateException ex) when (
                                ex.InnerExceptions.Count > 0 &&
                                ex.InnerExceptions.All(static e => e is OptionsValidationException))
                            {
                                // A validator (e.g. the built-in one) may itself aggregate multiple failing
                                // option instances; flatten so every failure is reported together.
                                (validationFailures ??= new()).AddRange(ex.InnerExceptions);
                            }
                            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
                            {
                                // Preserve StartAsync cancellation semantics: cancellation of the startup token
                                // propagates as OperationCanceledException rather than being aggregated.
                                throw;
                            }
                            catch (Exception ex)
                            {
                                // An unexpected (non-validation) failure stops further validation, but any
                                // validation failures already collected are retained and reported alongside it.
                                (validationFailures ??= new()).Add(ex);
                                break;
                            }
                        }

                        if (validationFailures is not null)
                        {
                            if (validationFailures.Count == 1)
                            {
                                ExceptionDispatchInfo.Capture(validationFailures[0]).Throw();
                            }

                            if (validationFailures.Count > 1)
                            {
                                throw new AggregateException(validationFailures);
                            }
                        }
                    }

                    _hostedServices ??= Services.GetRequiredService<IEnumerable<IHostedService>>();
                    _hostedLifecycleServices = GetHostLifecycles(_hostedServices);
                }
                catch (Exception ex)
                {
                    if (ex is OperationCanceledException)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                    }

                    // service factory or validation failed, abort startup.
                    exceptions.Add(ex);
                    LogAndRethrow();
                    return; // unreachable
                }


                // Call StartingAsync().
                if (_hostedLifecycleServices is not null)
                {
                    await ForeachService(_hostedLifecycleServices, cancellationToken, concurrent, abortOnFirstException, exceptions,
                        (service, token) => service.StartingAsync(token)).ConfigureAwait(false);

                    // Exceptions in StartingAsync cause startup to be aborted.
                    LogAndRethrow();
                }

                // Call StartAsync().
                await ForeachService(_hostedServices, cancellationToken, concurrent, abortOnFirstException, exceptions,
                    async (service, token) =>
                    {
                        await service.StartAsync(token).ConfigureAwait(false);

                        if (service is BackgroundService backgroundService)
                        {
                            Task monitorTask = TryExecuteBackgroundServiceAsync(backgroundService);
                            List<Task> bgTasks = LazyInitializer.EnsureInitialized(ref _backgroundServiceTasks);
                            lock (bgTasks)
                            {
                                bgTasks.Add(monitorTask);
                            }
                        }
                    }).ConfigureAwait(false);

                // Exceptions in StartAsync cause startup to be aborted.
                LogAndRethrow();

                // Call StartedAsync().
                if (_hostedLifecycleServices is not null)
                {
                    await ForeachService(_hostedLifecycleServices, cancellationToken, concurrent, abortOnFirstException, exceptions,
                        (service, token) => service.StartedAsync(token)).ConfigureAwait(false);
                }

                // Exceptions in StartedAsync cause startup to be aborted.
                LogAndRethrow();

                // Cancel IHostApplicationLifetime.ApplicationStarted
                // This catches all exceptions and does not re-throw.
                _applicationLifetime.NotifyStarted();

                // Log and abort if there are exceptions.
                void LogAndRethrow()
                {
                    if (exceptions.Count > 0)
                    {
                        if (exceptions.Count == 1)
                        {
                            // Rethrow if it's a single error
                            Exception singleException = exceptions[0];
                            _logger.HostedServiceStartupFaulted(singleException);
                            ExceptionDispatchInfo.Capture(singleException).Throw();
                        }
                        else
                        {
                            var ex = new AggregateException("One or more hosted services failed to start.", exceptions);
                            _logger.HostedServiceStartupFaulted(ex);
                            throw ex;
                        }
                    }
                }
            }

            _logger.Started();
        }

        private async Task TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
        {
            // backgroundService.ExecuteTask may not be set (e.g. if the derived class doesn't call base.StartAsync)
            Task? backgroundTask = backgroundService.ExecuteTask;
            if (backgroundTask is null)
            {
                return;
            }

            try
            {
                await backgroundTask.ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                // When the host is being stopped, it cancels the background services.
                // This isn't an error condition, so don't log it as an error.
                if (_applicationLifetime.ApplicationStopping.IsCancellationRequested && backgroundTask.IsCanceled && ex is OperationCanceledException)
                {
                    return;
                }

                _logger.BackgroundServiceFaulted(ex);
                if (_options.BackgroundServiceExceptionBehavior == BackgroundServiceExceptionBehavior.StopHost)
                {
                    _logger.BackgroundServiceStoppingHost(ex);
                    List<Exception> exceptions = LazyInitializer.EnsureInitialized(ref _backgroundServiceExceptions);
                    lock (exceptions)
                    {
                        exceptions.Add(ex);
                    }

                    // This catches all exceptions and does not re-throw.
                    _applicationLifetime.StopApplication();
                }
            }
        }

        /// <summary>
        /// Order:
        ///  IHostedLifecycleService.StoppingAsync
        ///  IHostApplicationLifetime.ApplicationStopping
        ///  IHostedService.Stop
        ///  IHostedLifecycleService.StoppedAsync
        ///  IHostApplicationLifetime.ApplicationStopped
        ///  IHostLifetime.StopAsync
        /// </summary>
        public async Task StopAsync(CancellationToken cancellationToken = default)
        {
            _logger.Stopping();

            CancellationTokenSource? cts = null;
            if (_options.ShutdownTimeout != Timeout.InfiniteTimeSpan)
            {
                cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
                cts.CancelAfter(_options.ShutdownTimeout);
                cancellationToken = cts.Token;
            }

            using (cts)
            {
                List<Exception> exceptions = new();
                if (!_hostStarting || _hostedServices is null) // Started (and hosted services resolved)?
                {

                    // Cancel IHostApplicationLifetime.ApplicationStopping.
                    // This catches all exceptions and does not re-throw.
                    _applicationLifetime.StopApplication();
                }
                else
                {
                    Debug.Assert(_hostedServices != null, "Hosted services are resolved when host is started.");

                    // Ensure hosted services are stopped in LIFO order
                    IEnumerable<IHostedService> reversedServices = _hostedServices.Reverse();
                    IEnumerable<IHostedLifecycleService>? reversedLifetimeServices = _hostedLifecycleServices?.Reverse();
                    bool concurrent = _options.ServicesStopConcurrently;

                    // Call StoppingAsync().
                    if (reversedLifetimeServices is not null)
                    {
                        await ForeachService(reversedLifetimeServices, cancellationToken, concurrent, abortOnFirstException: false, exceptions,
                            (service, token) => service.StoppingAsync(token)).ConfigureAwait(false);
                    }

                    // Cancel IHostApplicationLifetime.ApplicationStopping.
                    // This catches all exceptions and does not re-throw.
                    _applicationLifetime.StopApplication();

                    // Call StopAsync().
                    await ForeachService(reversedServices, cancellationToken, concurrent, abortOnFirstException: false, exceptions, (service, token) =>
                        service.StopAsync(token)).ConfigureAwait(false);

                    // Call StoppedAsync().
                    if (reversedLifetimeServices is not null)
                    {
                        await ForeachService(reversedLifetimeServices, cancellationToken, concurrent, abortOnFirstException: false, exceptions, (service, token) =>
                            service.StoppedAsync(token)).ConfigureAwait(false);
                    }
                }

                // Cancel IHostApplicationLifetime.ApplicationStopped.
                // This catches all exceptions and does not re-throw.
                _applicationLifetime.NotifyStopped();

                // This may not catch exceptions, so we do it here.
                try
                {
                    await _hostLifetime.StopAsync(cancellationToken).ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                }

                _hostStopped = true;

                // Ensure all background service monitoring tasks have finished processing
                // exceptions before we read them. Without this, there's a race: when a
                // BackgroundService's ExecuteTask faults, both BackgroundService.StopAsync
                // (which Host awaits) and TryExecuteBackgroundServiceAsync (fire-and-forget)
                // have continuations scheduled. If StopAsync's continuation runs first, the
                // Host may read _backgroundServiceExceptions before the monitoring task has
                // added its exception.
                if (_backgroundServiceTasks is not null)
                {
                    Task bgMonitoringTasks = Task.WhenAll(_backgroundServiceTasks);
                    var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
                    using (cancellationToken.Register(s => ((TaskCompletionSource<object?>)s!).TrySetCanceled(), tcs))
                    {
                        await Task.WhenAny(bgMonitoringTasks, tcs.Task).ConfigureAwait(false);
                    }
                }

                // If background services faulted and caused the host to stop, rethrow the exceptions
                // so they propagate and cause a non-zero exit code.
                List<Exception>? backgroundServiceExceptions = Volatile.Read(ref _backgroundServiceExceptions);
                if (backgroundServiceExceptions is not null)
                {
                    lock (backgroundServiceExceptions)
                    {
                        exceptions.AddRange(backgroundServiceExceptions);
                    }
                }

                if (exceptions.Count > 0)
                {
                    if (exceptions.Count == 1)
                    {
                        // Rethrow if it's a single error
                        Exception singleException = exceptions[0];
                        _logger.StoppedWithException(singleException);
                        ExceptionDispatchInfo.Capture(singleException).Throw();
                    }
                    else
                    {
                        var ex = new AggregateException("One or more hosted services failed to stop or one or more background services threw an exception.", exceptions);
                        _logger.StoppedWithException(ex);
                        throw ex;
                    }
                }
            }

            _logger.Stopped();
        }

        private static async Task ForeachService<T>(
            IEnumerable<T> services,
            CancellationToken token,
            bool concurrent,
            bool abortOnFirstException,
            List<Exception> exceptions,
            Func<T, CancellationToken, Task> operation)
        {
            if (concurrent)
            {
                // The beginning synchronous portions of the implementations are run serially in registration order for
                // performance since it is common to return Task.Completed as a noop.
                // Any subsequent asynchronous portions are grouped together and run concurrently.
                List<Task>? tasks = null;

                foreach (T service in services)
                {
                    Task task;
                    try
                    {
                        task = operation(service, token);
                    }
                    catch (Exception ex)
                    {
                        exceptions.Add(ex); // Log exception from sync method.
                        continue;
                    }

                    if (task.IsCompleted)
                    {
                        if (task.Exception is not null)
                        {
                            exceptions.AddRange(task.Exception.InnerExceptions); // Log exception from async method.
                        }
                        else if (task.IsCanceled)
                        {
                            exceptions.Add(new TaskCanceledException(task));
                        }
                    }
                    else
                    {
                        // The task encountered an await; add it to a list to run concurrently.
                        tasks ??= new();
                        tasks.Add(task);
                    }
                }

                if (tasks is not null)
                {
                    Task groupedTasks = Task.WhenAll(tasks);

                    try
                    {
                        await groupedTasks.ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        if (groupedTasks.IsFaulted)
                        {
                            exceptions.AddRange(groupedTasks.Exception.InnerExceptions);
                        }
                        else
                        {
                            exceptions.Add(ex);
                        }
                    }
                }
            }
            else
            {
                foreach (T service in services)
                {
                    try
                    {
                        await operation(service, token).ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        exceptions.Add(ex);
                        if (abortOnFirstException)
                        {
                            return;
                        }
                    }
                }
            }
        }

        private static List<IHostedLifecycleService>? GetHostLifecycles(IEnumerable<IHostedService> hostedServices)
        {
            List<IHostedLifecycleService>? _result = null;

            foreach (IHostedService hostedService in hostedServices)
            {
                if (hostedService is IHostedLifecycleService service)
                {
                    _result ??= new List<IHostedLifecycleService>();
                    _result.Add(service);
                }
            }

            return _result;
        }

        public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();

        public async ValueTask DisposeAsync()
        {
            IFileProvider contentRootFileProvider = _hostEnvironment.ContentRootFileProvider;
            await DisposeAsync(contentRootFileProvider).ConfigureAwait(false);

            if (!ReferenceEquals(contentRootFileProvider, _defaultProvider))
            {
                // In the rare case that the user replaced the ContentRootFileProvider, dispose it and the one
                // we originally created
                await DisposeAsync(_defaultProvider).ConfigureAwait(false);
            }

            // Dispose the service provider
            await DisposeAsync(Services).ConfigureAwait(false);

            static ValueTask DisposeAsync(object o)
            {
                switch (o)
                {
                    case IAsyncDisposable asyncDisposable:
                        return asyncDisposable.DisposeAsync();
                    case IDisposable disposable:
                        disposable.Dispose();
                        break;
                }
                return default;
            }
        }

        private string DebuggerToString()
        {
            return $@"ApplicationName = ""{_hostEnvironment.ApplicationName}"", IsRunning = {(IsRunning ? "true" : "false")}";
        }

        // Host is running if the app has been started and the host hasn't been stopped.
        private bool IsRunning => _applicationLifetime.ApplicationStarted.IsCancellationRequested && !_hostStopped;

        internal sealed class HostDebugView(Host host)
        {
            public IServiceProvider Services => host.Services;
            public IConfiguration Configuration => host.Services.GetRequiredService<IConfiguration>();
            public IHostEnvironment Environment => host._hostEnvironment;
            public IHostApplicationLifetime ApplicationLifetime => host._applicationLifetime;
            public HostOptions Options => host._options;
            // _hostedServices is null until the host is started. Resolve services directly from DI if host hasn't started yet.
            // Want to resolve hosted services once because it's possible they might have been registered with a transient lifetime.
            public List<IHostedService> HostedServices => new List<IHostedService>(host._hostedServices ??= host.Services.GetRequiredService<IEnumerable<IHostedService>>());
            public bool IsRunning => host.IsRunning;
        }
    }
}