From bc70792690937e88457ec3e0c771a48a1668ac30 Mon Sep 17 00:00:00 2001
From: Steve Harter <steveharter@users.noreply.github.com>
Date: Fri, 9 Jun 2023 13:29:29 -0500
Subject: [PATCH 1/5] Add IHostedLifecycleService

commit d0e9189a6fe21b3e604dc3188a804d0cb252125d
Author: Steve Harter <steveharter@users.noreply.github.com>
Date:   Fri Jun 9 13:20:30 2023 -0500

    sync

commit a5e88d58c15be5f3fe63e4d3f994448054ca8dbe
Merge: 8010ffa392c 9b7db0f4436
Author: Steve Harter <steveharter@users.noreply.github.com>
Date:   Fri Jun 9 10:59:47 2023 -0500

    Merge branch 'main' of https://github.com/steveharter/runtime into Startup

commit 8010ffa392cc10ceb96ed84c7a1c571a7dc47aac
Author: Steve Harter <steveharter@users.noreply.github.com>
Date:   Fri May 19 10:39:31 2023 -0500

    Add IServiceStartup to enable pre-startup hooks
---
 ...crosoft.Extensions.Hosting.Abstractions.cs |   7 +
 .../src/IHostedLifecycleService.cs            |  44 ++
 .../ref/Microsoft.Extensions.Hosting.cs       |   1 +
 .../src/HostOptions.cs                        |  25 +-
 .../src/Internal/Host.cs                      | 431 +++++++++++++++---
 .../tests/UnitTests/DisposeTests.cs           |  99 ++++
 .../tests/UnitTests/LifecycleTests.Start.cs   | 341 ++++++++++++++
 .../tests/UnitTests/LifecycleTests.Stop.cs    | 352 ++++++++++++++
 .../UnitTests/LifecycleTests.Timeouts.cs      | 169 +++++++
 .../tests/UnitTests/LifecycleTests.cs         | 177 +++++++
 10 files changed, 1573 insertions(+), 73 deletions(-)
 create mode 100644 src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostedLifecycleService.cs
 create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DisposeTests.cs
 create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs
 create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs
 create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Timeouts.cs
 create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs

diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs
index 88f37fc9546228..cc663649830ad8 100644
--- a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs
@@ -124,6 +124,13 @@ public partial interface IHostBuilder
         Microsoft.Extensions.Hosting.IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Microsoft.Extensions.DependencyInjection.IServiceProviderFactory<TContainerBuilder> factory) where TContainerBuilder : notnull;
         Microsoft.Extensions.Hosting.IHostBuilder UseServiceProviderFactory<TContainerBuilder>(System.Func<Microsoft.Extensions.Hosting.HostBuilderContext, Microsoft.Extensions.DependencyInjection.IServiceProviderFactory<TContainerBuilder>> factory) where TContainerBuilder : notnull;
     }
+    public partial interface IHostedLifecycleService : Microsoft.Extensions.Hosting.IHostedService
+    {
+        System.Threading.Tasks.Task StartedAsync(System.Threading.CancellationToken cancellationToken);
+        System.Threading.Tasks.Task StartingAsync(System.Threading.CancellationToken cancellationToken);
+        System.Threading.Tasks.Task StoppedAsync(System.Threading.CancellationToken cancellationToken);
+        System.Threading.Tasks.Task StoppingAsync(System.Threading.CancellationToken cancellationToken);
+    }
     public partial interface IHostedService
     {
         System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken);
diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostedLifecycleService.cs b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostedLifecycleService.cs
new file mode 100644
index 00000000000000..114308606e80fd
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostedLifecycleService.cs
@@ -0,0 +1,44 @@
+﻿// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace Microsoft.Extensions.Hosting
+{
+    /// <summary>
+    /// Defines methods that are run before or after
+    /// <see cref="IHostedService.StartAsync(CancellationToken)"/> and
+    /// <see cref="IHostedService.StopAsync(CancellationToken)"/>.
+    /// </summary>
+    public interface IHostedLifecycleService : IHostedService
+    {
+        /// <summary>
+        /// Triggered before <see cref="IHostedService.StartAsync(CancellationToken)"/>.
+        /// </summary>
+        /// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
+        Task StartingAsync(CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Triggered after <see cref="IHostedService.StartAsync(CancellationToken)"/>.
+        /// </summary>
+        /// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
+        Task StartedAsync(CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Triggered before <see cref="IHostedService.StopAsync(CancellationToken)"/>.
+        /// </summary>
+        /// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
+        Task StoppingAsync(CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Triggered after <see cref="IHostedService.StopAsync(CancellationToken)"/>.
+        /// </summary>
+        /// <param name="cancellationToken">Indicates that the stop process has been aborted.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
+        Task StoppedAsync(CancellationToken cancellationToken);
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs
index 4e7804b87d1ca8..0085fcae802df6 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs
@@ -110,6 +110,7 @@ public HostOptions() { }
         public bool ServicesStartConcurrently { get { throw null; } set { } }
         public bool ServicesStopConcurrently { get { throw null; } set { } }
         public System.TimeSpan ShutdownTimeout { get { throw null; } set { } }
+        public System.TimeSpan StartupTimeout { get { throw null; } set { } }
     }
 }
 namespace Microsoft.Extensions.Hosting.Internal
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs
index c6c29dc10cfb2f..92d27878a9415c 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Globalization;
+using System.Threading;
 using Microsoft.Extensions.Configuration;
 
 namespace Microsoft.Extensions.Hosting
@@ -13,10 +14,25 @@ namespace Microsoft.Extensions.Hosting
     public class HostOptions
     {
         /// <summary>
-        /// The default timeout for <see cref="IHost.StopAsync(System.Threading.CancellationToken)"/>.
+        /// The default timeout for <see cref="IHost.StopAsync(CancellationToken)"/>.
         /// </summary>
+        /// <remarks>
+        /// This timeout also encompasses all host services implementing
+        /// <see cref="IHostedLifecycleService.StoppingAsync(CancellationToken)"/> and
+        /// <see cref="IHostedLifecycleService.StoppedAsync(CancellationToken)"/>.
+        /// </remarks>
         public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30);
 
+        /// <summary>
+        /// The default timeout for <see cref="IHost.StartAsync(CancellationToken)"/>.
+        /// </summary>
+        /// <remarks>
+        /// This timeout also encompasses all host services implementing
+        /// <see cref="IHostedLifecycleService.StartingAsync(CancellationToken)"/> and
+        /// <see cref="IHostedLifecycleService.StartedAsync(CancellationToken)"/>.
+        /// </remarks>
+        public TimeSpan StartupTimeout { get; set; } = Timeout.InfiniteTimeSpan;
+
         /// <summary>
         /// Determines if the <see cref="IHost"/> will start registered instances of <see cref="IHostedService"/> concurrently or sequentially. Defaults to false.
         /// </summary>
@@ -46,6 +62,13 @@ internal void Initialize(IConfiguration configuration)
                 ShutdownTimeout = TimeSpan.FromSeconds(seconds);
             }
 
+            timeoutSeconds = configuration["startupTimeoutSeconds"];
+            if (!string.IsNullOrEmpty(timeoutSeconds)
+                && int.TryParse(timeoutSeconds, NumberStyles.None, CultureInfo.InvariantCulture, out seconds))
+            {
+                StartupTimeout = TimeSpan.FromSeconds(seconds);
+            }
+
             var servicesStartConcurrently = configuration["servicesStartConcurrently"];
             if (!string.IsNullOrEmpty(servicesStartConcurrently)
                 && bool.TryParse(servicesStartConcurrently, out bool startBehavior))
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
index 13b18f8e3cf153..2f98128525e966 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
@@ -23,6 +23,7 @@ internal sealed class Host : IHost, IAsyncDisposable
         private readonly IHostEnvironment _hostEnvironment;
         private readonly PhysicalFileProvider _defaultProvider;
         private IEnumerable<IHostedService>? _hostedServices;
+        private IEnumerable<IHostedLifecycleService>? _hostedLifecycleServices;
         private volatile bool _stopCalled;
 
         public Host(IServiceProvider services,
@@ -58,79 +59,87 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
         {
             _logger.Starting();
 
-            using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping);
-            CancellationToken combinedCancellationToken = combinedCancellationTokenSource.Token;
-
-            await _hostLifetime.WaitForStartAsync(combinedCancellationToken).ConfigureAwait(false);
-
-            combinedCancellationToken.ThrowIfCancellationRequested();
-            _hostedServices = Services.GetRequiredService<IEnumerable<IHostedService>>();
-
-            List<Exception> exceptions = new List<Exception>();
-
-            if (_options.ServicesStartConcurrently)
+            CancellationTokenSource? cts = null;
+            CancellationTokenSource linkedCts;
+            if (_options.StartupTimeout != Timeout.InfiniteTimeSpan)
             {
-                List<Task> tasks = new List<Task>();
+                cts = new CancellationTokenSource(_options.StartupTimeout);
+                linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken, _applicationLifetime.ApplicationStopping);
+            }
+            else
+            {
+                linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping);
+            }
 
-                foreach (IHostedService hostedService in _hostedServices)
-                {
-                    tasks.Add(Task.Run(() => StartAndTryToExecuteAsync(hostedService, combinedCancellationToken), combinedCancellationToken));
-                }
+            using (cts)
+            using (linkedCts)
+            {
+                CancellationToken token = linkedCts.Token;
+                List<Exception> exceptions = new();
+                _hostedServices = Services.GetRequiredService<IEnumerable<IHostedService>>();
+                _hostedLifecycleServices = GetHostLifecycles(_hostedServices);
 
-                Task groupedTasks = Task.WhenAll(tasks);
+                await _hostLifetime.WaitForStartAsync(token).ConfigureAwait(false);
+                token.ThrowIfCancellationRequested();
 
-                try
+                if (_hostedLifecycleServices is not null)
                 {
-                    await groupedTasks.ConfigureAwait(false);
+                    await CallLifeCycle_Starting(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false);
                 }
-                catch (Exception ex)
+
+                // Run IHostedService.Start either concurrently or serially.
+                if (_options.ServicesStartConcurrently)
                 {
-                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+                    await CallLifeCycle_Start(_hostedServices, token, exceptions).ConfigureAwait(false);
                 }
-            }
-            else
-            {
-                foreach (IHostedService hostedService in _hostedServices)
+                else
                 {
-                    try
+                    foreach (IHostedService hostedService in _hostedServices)
                     {
-                        // Fire IHostedService.Start
-                        await StartAndTryToExecuteAsync(hostedService, combinedCancellationToken).ConfigureAwait(false);
-                    }
-                    catch (Exception ex)
-                    {
-                        exceptions.Add(ex);
-                        break;
+                        try
+                        {
+                            await StartAndTryToExecuteAsync(hostedService, token).ConfigureAwait(false);
+                        }
+                        catch (Exception ex)
+                        {
+                            exceptions.Add(ex);
+                            break;
+                        }
                     }
                 }
-            }
 
-            if (exceptions.Count > 0)
-            {
-                if (exceptions.Count == 1)
+                if (_hostedLifecycleServices is not null)
                 {
-                    // Rethrow if it's a single error
-                    Exception singleException = exceptions[0];
-                    _logger.HostedServiceStartupFaulted(singleException);
-                    ExceptionDispatchInfo.Capture(singleException).Throw();
+                    await CallLifeCycle_Started(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false);
                 }
-                else
+
+                if (exceptions.Count > 0)
                 {
-                    var ex = new AggregateException("One or more hosted services failed to start.", exceptions);
-                    _logger.HostedServiceStartupFaulted(ex);
-                    throw ex;
+                    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;
+                    }
                 }
-            }
 
-            // Fire IHostApplicationLifetime.Started
-            _applicationLifetime.NotifyStarted();
+                // Fire IHostApplicationLifetime.Started
+                _applicationLifetime.NotifyStarted();
+            }
 
             _logger.Started();
         }
 
-        private async Task StartAndTryToExecuteAsync(IHostedService service, CancellationToken combinedCancellationToken)
+        private async Task StartAndTryToExecuteAsync(IHostedService service, CancellationToken token)
         {
-            await service.StartAsync(combinedCancellationToken).ConfigureAwait(false);
+            await service.StartAsync(token).ConfigureAwait(false);
 
             if (service is BackgroundService backgroundService)
             {
@@ -174,42 +183,46 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
             _stopCalled = true;
             _logger.Stopping();
 
-            using (var cts = new CancellationTokenSource(_options.ShutdownTimeout))
-            using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken))
+            CancellationTokenSource? cts = null;
+            CancellationTokenSource linkedCts;
+            if (_options.ShutdownTimeout != Timeout.InfiniteTimeSpan)
+            {
+                cts = new CancellationTokenSource(_options.ShutdownTimeout);
+                linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken);
+            }
+            else
+            {
+                linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            }
+
+            using (cts)
+            using (linkedCts)
             {
                 CancellationToken token = linkedCts.Token;
+                List<Exception> exceptions = new();
+
                 // Trigger IHostApplicationLifetime.ApplicationStopping
                 _applicationLifetime.StopApplication();
 
-                var exceptions = new List<Exception>();
                 if (_hostedServices != null) // Started?
                 {
                     // Ensure hosted services are stopped in LIFO order
-                    IEnumerable<IHostedService> hostedServices = _hostedServices.Reverse();
+                    IEnumerable<IHostedService> reversedServices = _hostedServices.Reverse();
+                    IEnumerable<IHostedLifecycleService>? reversedLifetimeServices = _hostedLifecycleServices?.Reverse();
 
-                    if (_options.ServicesStopConcurrently)
+                    if (reversedLifetimeServices is not null)
                     {
-                        List<Task> tasks = new List<Task>();
-
-                        foreach (IHostedService hostedService in hostedServices)
-                        {
-                            tasks.Add(Task.Run(() => hostedService.StopAsync(token), token));
-                        }
-
-                        Task groupedTasks = Task.WhenAll(tasks);
+                        await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false);
+                    }
 
-                        try
-                        {
-                            await groupedTasks.ConfigureAwait(false);
-                        }
-                        catch (Exception ex)
-                        {
-                            exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
-                        }
+                    // Run IHostedService.Stop either concurrently or serially.
+                    if (_options.ServicesStopConcurrently)
+                    {
+                        await CallLifeCycle_Stop(reversedServices, token, exceptions).ConfigureAwait(false);
                     }
                     else
                     {
-                        foreach (IHostedService hostedService in hostedServices)
+                        foreach (IHostedService hostedService in reversedServices)
                         {
                             try
                             {
@@ -221,6 +234,11 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
                             }
                         }
                     }
+
+                    if (reversedLifetimeServices is not null)
+                    {
+                        await CallLifeCycle_Stopped(reversedLifetimeServices, token, exceptions).ConfigureAwait(false);
+                    }
                 }
 
                 // Fire IHostApplicationLifetime.Stopped
@@ -256,6 +274,275 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
             _logger.Stopped();
         }
 
+        /// <summary>
+        /// Call <see cref="IHostedLifecycleService.StartingAsync(CancellationToken)"/>.
+        /// 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 run concurrently.
+        /// </summary>
+        private static async Task CallLifeCycle_Starting(IEnumerable<IHostedLifecycleService> services, CancellationToken token, List<Exception> exceptions)
+        {
+            List<Task>? tasks = null;
+
+            foreach (IHostedLifecycleService service in services)
+            {
+                Task? task =  task = service.StartingAsync(token);
+                if (task.IsCompleted)
+                {
+                    if (task.Exception is not null)
+                    {
+                        exceptions.Add(task.Exception);
+                    }
+                }
+                else
+                {
+                    tasks ??= new();
+                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
+                }
+            }
+
+            if (tasks is not null)
+            {
+                Task groupedTasks = Task.WhenAll(tasks);
+
+                try
+                {
+                    await groupedTasks.ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+                }
+            }
+        }
+
+        /// <summary>
+        /// Call <see cref="IHostedService.StartAsync(CancellationToken)"/>.
+        /// 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 run concurrently.
+        /// </summary>
+        private static async Task CallLifeCycle_Start(IEnumerable<IHostedService> services, CancellationToken token, List<Exception> exceptions)
+        {
+            List<Task>? tasks = null;
+
+            foreach (IHostedService service in services)
+            {
+                Task? task = service.StartAsync(token);
+                if (task.IsCompleted)
+                {
+                    if (task.Exception is not null)
+                    {
+                        exceptions.Add(task.Exception);
+                    }
+                }
+                else
+                {
+                    tasks ??= new();
+                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
+                }
+            }
+
+            if (tasks is not null)
+            {
+                Task groupedTasks = Task.WhenAll(tasks);
+
+                try
+                {
+                    await groupedTasks.ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+                }
+            }
+        }
+
+        /// <summary>
+        /// Call <see cref="IHostedLifecycleService.StartedAsync(CancellationToken)"/>.
+        /// 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 run concurrently.
+        /// </summary>
+        private static async Task CallLifeCycle_Started(IEnumerable<IHostedLifecycleService> services, CancellationToken token, List<Exception> exceptions)
+        {
+            List<Task>? tasks = null;
+
+            foreach (IHostedLifecycleService service in services)
+            {
+                Task? task = task = service.StartedAsync(token);
+                if (task.IsCompleted)
+                {
+                    if (task.Exception is not null)
+                    {
+                        exceptions.Add(task.Exception);
+                    }
+                }
+                else
+                {
+                    tasks ??= new();
+                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
+                }
+
+            }
+
+            if (tasks is not null)
+            {
+                Task groupedTasks = Task.WhenAll(tasks);
+
+                try
+                {
+                    await groupedTasks.ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+                }
+            }
+        }
+
+        /// <summary>
+        /// Call <see cref="IHostedLifecycleService.StoppingAsync(CancellationToken)"/>.
+        /// 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 run concurrently.
+        /// </summary>
+        private static async Task CallLifeCycle_Stopping(IEnumerable<IHostedLifecycleService> services, CancellationToken token, List<Exception> exceptions)
+        {
+            List<Task>? tasks = null;
+
+            foreach (IHostedLifecycleService service in services)
+            {
+                Task? task = task = service.StoppingAsync(token);
+                if (task.IsCompleted)
+                {
+                    if (task.Exception is not null)
+                    {
+                        exceptions.Add(task.Exception);
+                    }
+                }
+                else
+                {
+                    tasks ??= new();
+                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
+                }
+            }
+
+            if (tasks is not null)
+            {
+                Task groupedTasks = Task.WhenAll(tasks);
+
+                try
+                {
+                    await groupedTasks.ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+                }
+            }
+        }
+
+        /// <summary>
+        /// Call <see cref="IHostedService.StopAsync(CancellationToken)"/>.
+        /// 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 run concurrently.
+        /// </summary>
+        private static async Task CallLifeCycle_Stop(IEnumerable<IHostedService> services, CancellationToken token, List<Exception> exceptions)
+        {
+            List<Task>? tasks = null;
+
+            foreach (IHostedService service in services)
+            {
+                Task? task = task = service.StopAsync(token);
+                if (task.IsCompleted)
+                {
+                    if (task.Exception is not null)
+                    {
+                        exceptions.Add(task.Exception);
+                    }
+                }
+                else
+                {
+                    tasks ??= new();
+                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
+                }
+            }
+
+            if (tasks is not null)
+            {
+                Task groupedTasks = Task.WhenAll(tasks);
+
+                try
+                {
+                    await groupedTasks.ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+                }
+            }
+        }
+
+        /// <summary>
+        /// Call <see cref="IHostedLifecycleService.StoppedAsync(CancellationToken)"/>.
+        /// 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 run concurrently.
+        /// </summary>
+        private static async Task CallLifeCycle_Stopped(IEnumerable<IHostedLifecycleService> services, CancellationToken token, List<Exception> exceptions)
+        {
+            List<Task>? tasks = null;
+
+            foreach (IHostedLifecycleService service in services)
+            {
+                Task? task = task = service.StoppedAsync(token);
+                if (task.IsCompleted)
+                {
+                    if (task.Exception is not null)
+                    {
+                        exceptions.Add(task.Exception);
+                    }
+                }
+                else
+                {
+                    tasks ??= new();
+                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
+                }
+            }
+
+            if (tasks is not null)
+            {
+                Task groupedTasks = Task.WhenAll(tasks);
+
+                try
+                {
+                    await groupedTasks.ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+                }
+            }
+        }
+
+        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()
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DisposeTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DisposeTests.cs
new file mode 100644
index 00000000000000..30e777ec753eaf
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DisposeTests.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;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.Extensions.Hosting.Tests
+{
+    public partial class DisposeTests
+    {
+        public static IHostBuilder CreateHostBuilder(Action<IServiceCollection> configure)
+        {
+            return new HostBuilder().ConfigureServices(configure);
+        }
+
+        [Fact]
+        public void DisposeCalled_Interface()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.AddSingleton<IMyService>((sp) => new MyService());
+            });
+
+            IMyService obj;
+            using (var host = hostBuilder.Build())
+            {
+                obj = host.Services.GetService<IMyService>();
+            }
+
+            Assert.True(obj.IsDisposed);
+        }
+
+        [Fact]
+        public void DisposeCalled_Class()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.AddSingleton<MyService>();
+            });
+
+            MyService obj;
+            using (var host = hostBuilder.Build())
+            {
+                obj = host.Services.GetService<MyService>();
+            }
+
+            Assert.True(obj.IsDisposed);
+        }
+
+        [Fact]
+        public void DisposeNotCalled()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.AddSingleton(new MyService());
+            });
+
+            MyService obj;
+            using (var host = hostBuilder.Build())
+            {
+                obj = host.Services.GetService<MyService>();
+            }
+
+            Assert.False(obj.IsDisposed);
+        }
+
+        public interface IMyService : IDisposable
+        {
+            bool IsDisposed { get; }
+        }
+
+        public class MyService : IMyService
+        {
+            private bool _isDisposed;
+
+            public MyService()
+            {
+            }
+
+            public bool IsDisposed => _isDisposed;
+
+            protected virtual void Dispose(bool disposing)
+            {
+                if (!_isDisposed)
+                {
+                    _isDisposed = true;
+                }
+            }
+
+            public void Dispose()
+            {
+                Dispose(disposing: true);
+                GC.SuppressFinalize(this);
+            }
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs
new file mode 100644
index 00000000000000..61d5ce9583c651
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs
@@ -0,0 +1,341 @@
+﻿// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.Extensions.Hosting.Tests
+{
+    public partial class LifecycleTests
+    {
+        [Fact]
+        public async Task StartingConcurrently()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services
+                    .AddHostedService<StartingTestClass<Impl1>>()
+                    .AddHostedService<StartingTestClass<Impl2>>();
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                StartingTestClass<Impl1>.s_wait1.Wait();
+                StartingTestClass<Impl2>.s_wait1.Wait();
+                StartingTestClass<Impl1>.s_wait2.Wait();
+                StartingTestClass<Impl2>.s_wait2.Wait();
+                StartingTestClass<Impl1>.s_wait3.Wait();
+                StartingTestClass<Impl2>.s_wait3.Wait();
+
+                Verify(0, 0, 0, 0, 0, 0);
+
+                // Both run serially until the await.
+                Task start = host.StartAsync();
+                Verify(1, 1, 0, 0, 0, 0);
+                await Task.Delay(s_superShortDelay);
+                Verify(1, 1, 0, 0, 0, 0);
+
+                // Resume and check that both are not finished.
+                StartingTestClass<Impl1>.s_wait1.Release();
+                await StartingTestClass<Impl1>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 0, 0, 0);
+
+                StartingTestClass<Impl2>.s_wait1.Release();
+                await StartingTestClass<Impl2>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 1, 0, 0);
+
+                // Resume and verify they finish.
+                StartingTestClass<Impl1>.s_wait3.Release();
+                StartingTestClass<Impl2>.s_wait3.Release();
+                await start;
+                Verify(1, 1, 1, 1, 1, 1);
+                Assert.True(start.IsCompleted);
+            }
+
+            void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2)
+            {
+                Assert.Equal(initial1, StartingTestClass<Impl1>.s_initialCount);
+                Assert.Equal(initial2, StartingTestClass<Impl2>.s_initialCount);
+                Assert.Equal(pause1, StartingTestClass<Impl1>.s_pauseCount);
+                Assert.Equal(pause2, StartingTestClass<Impl2>.s_pauseCount);
+                Assert.Equal(final1, StartingTestClass<Impl1>.s_finalCount);
+                Assert.Equal(final2, StartingTestClass<Impl2>.s_finalCount);
+            }
+        }
+
+        private class StartingTestClass<T> : IHostedLifecycleService
+        {
+            public static int s_initialCount = 0;
+            public static int s_pauseCount = 0;
+            public static int s_finalCount = 0;
+            public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1);
+
+            public async Task StartingAsync(CancellationToken cancellationToken)
+            {
+                s_initialCount++;
+                await s_wait1.WaitAsync();
+                s_pauseCount++;
+                s_wait2.Release();
+                await s_wait3.WaitAsync();
+                s_finalCount++;
+            }
+            public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+
+        [Fact]
+        public async Task StartConcurrently()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.
+                    AddHostedService<StartTestClass<Impl1>>().
+                    AddHostedService<StartTestClass<Impl2>>().
+                    Configure<HostOptions>(opts => opts.ServicesStartConcurrently = true);
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                StartTestClass<Impl1>.s_wait1.Wait();
+                StartTestClass<Impl2>.s_wait1.Wait();
+                StartTestClass<Impl1>.s_wait2.Wait();
+                StartTestClass<Impl2>.s_wait2.Wait();
+                StartTestClass<Impl1>.s_wait3.Wait();
+                StartTestClass<Impl2>.s_wait3.Wait();
+
+                Verify(0, 0, 0, 0, 0, 0);
+
+                // Both run serially until the await.
+                Task start = host.StartAsync();
+                Verify(1, 1, 0, 0, 0, 0);
+                await Task.Delay(s_superShortDelay);
+                Verify(1, 1, 0, 0, 0, 0);
+
+                // Resume and check that both are not finished.
+                StartTestClass<Impl1>.s_wait1.Release();
+                await StartTestClass<Impl1>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 0, 0, 0);
+
+                StartTestClass<Impl2>.s_wait1.Release();
+                await StartTestClass<Impl2>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 1, 0, 0);
+
+                // Resume and verify they finish.
+                StartTestClass<Impl1>.s_wait3.Release();
+                StartTestClass<Impl2>.s_wait3.Release();
+                await start;
+                Verify(1, 1, 1, 1, 1, 1);
+                Assert.True(start.IsCompleted);
+            }
+
+            void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2)
+            {
+                Assert.Equal(initial1, StartTestClass<Impl1>.s_initialCount);
+                Assert.Equal(initial2, StartTestClass<Impl2>.s_initialCount);
+                Assert.Equal(pause1, StartTestClass<Impl1>.s_pauseCount);
+                Assert.Equal(pause2, StartTestClass<Impl2>.s_pauseCount);
+                Assert.Equal(final1, StartTestClass<Impl1>.s_finalCount);
+                Assert.Equal(final2, StartTestClass<Impl2>.s_finalCount);
+            }
+        }
+
+        private class StartTestClass<T> : IHostedLifecycleService
+        {
+            public static int s_initialCount = 0;
+            public static int s_pauseCount = 0;
+            public static int s_finalCount = 0;
+            public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1);
+
+            public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public async Task StartAsync(CancellationToken cancellationToken)
+            {
+                s_initialCount++;
+                await s_wait1.WaitAsync();
+                s_pauseCount++;
+                s_wait2.Release();
+                await s_wait3.WaitAsync();
+                s_finalCount++;
+            }
+            public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+
+        [Fact]
+        public async Task StartNonconcurrently()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.
+                    AddHostedService<StartNonconcurrentTestClass<Impl1>>().
+                    AddHostedService<StartNonconcurrentTestClass<Impl2>>().
+                    Configure<HostOptions>(opts => opts.ServicesStartConcurrently = false);
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                StartNonconcurrentTestClass<Impl1>.s_wait.Wait();
+                StartNonconcurrentTestClass<Impl2>.s_wait.Wait();
+
+                Verify(0, 0);
+
+                // Both run serially.
+                Task start = host.StartAsync();
+                Verify(1, 0);
+                await Task.Delay(s_superShortDelay);
+                Verify(1, 0);
+
+                // Resume and verify they finish.
+                StartNonconcurrentTestClass<Impl1>.s_wait.Release();
+                StartNonconcurrentTestClass<Impl2>.s_wait.Release();
+                await start;
+                Verify(1, 1);
+                Assert.True(start.IsCompleted);
+            }
+
+            void Verify(int count1, int count2)
+            {
+                Assert.Equal(count1, StartNonconcurrentTestClass<Impl1>.s_count);
+                Assert.Equal(count2, StartNonconcurrentTestClass<Impl2>.s_count);
+            }
+        }
+
+        private class StartNonconcurrentTestClass<T> : IHostedLifecycleService
+        {
+            public static int s_count = 0;
+            public static SemaphoreSlim? s_wait = new SemaphoreSlim(1);
+
+            public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public async Task StartAsync(CancellationToken cancellationToken)
+            {
+                s_count++;
+                await s_wait.WaitAsync();
+            }
+            public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+
+        [Fact]
+        public async Task StartedConcurrently()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services
+                    .AddHostedService<StartedTestClass<Impl1>>()
+                    .AddHostedService<StartedTestClass<Impl2>>();
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                StartedTestClass<Impl1>.s_wait1.Wait();
+                StartedTestClass<Impl2>.s_wait1.Wait();
+                StartedTestClass<Impl1>.s_wait2.Wait();
+                StartedTestClass<Impl2>.s_wait2.Wait();
+                StartedTestClass<Impl1>.s_wait3.Wait();
+                StartedTestClass<Impl2>.s_wait3.Wait();
+
+                Verify(0, 0, 0, 0, 0, 0);
+
+                // Both run serially until the await.
+                Task start = host.StartAsync();
+                Verify(1, 1, 0, 0, 0, 0);
+                await Task.Delay(s_superShortDelay);
+                Verify(1, 1, 0, 0, 0, 0);
+
+                // Resume and check that both are not finished.
+                StartedTestClass<Impl1>.s_wait1.Release();
+                await StartedTestClass<Impl1>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 0, 0, 0);
+
+                StartedTestClass<Impl2>.s_wait1.Release();
+                await StartedTestClass<Impl2>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 1, 0, 0);
+
+                // Resume and verify they finish.
+                StartedTestClass<Impl1>.s_wait3.Release();
+                StartedTestClass<Impl2>.s_wait3.Release();
+                await start;
+                Verify(1, 1, 1, 1, 1, 1);
+                Assert.True(start.IsCompleted);
+            }
+
+            void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2)
+            {
+                Assert.Equal(initial1, StartedTestClass<Impl1>.s_initialCount);
+                Assert.Equal(initial2, StartedTestClass<Impl2>.s_initialCount);
+                Assert.Equal(pause1, StartedTestClass<Impl1>.s_pauseCount);
+                Assert.Equal(pause2, StartedTestClass<Impl2>.s_pauseCount);
+                Assert.Equal(final1, StartedTestClass<Impl1>.s_finalCount);
+                Assert.Equal(final2, StartedTestClass<Impl2>.s_finalCount);
+            }
+        }
+
+        private class StartedTestClass<T> : IHostedLifecycleService
+        {
+            public static int s_initialCount = 0;
+            public static int s_pauseCount = 0;
+            public static int s_finalCount = 0;
+            public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1);
+
+            public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public async Task StartedAsync(CancellationToken cancellationToken)
+            {
+                s_initialCount++;
+                await s_wait1.WaitAsync();
+                s_pauseCount++;
+                s_wait2.Release();
+                await s_wait3.WaitAsync();
+                s_finalCount++;
+            }
+            public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task StartPhasesException(bool throwAfterAsyncCall)
+        {
+            ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, true, true, true, false, false, false);
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.AddHostedService((token) => impl);
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                AggregateException ex = await Assert.ThrowsAnyAsync<AggregateException>(async () => await host.StartAsync());
+
+                Assert.True(impl.StartingCalled);
+                Assert.True(impl.StartCalled);
+                Assert.True(impl.StartedCalled);
+
+                Assert.Contains("(ThrowOnStarting)", ex.Message);
+                Assert.Contains("(ThrowOnStart)", ex.Message);
+                Assert.Contains("(ThrowOnStarted)", ex.Message);
+
+                Assert.Equal(3, ex.InnerExceptions.Count);
+                Assert.Contains("(ThrowOnStarting)", ex.InnerExceptions[0].Message);
+                Assert.Contains("(ThrowOnStart)", ex.InnerExceptions[1].Message);
+                Assert.Contains("(ThrowOnStarted)", ex.InnerExceptions[2].Message);
+            }
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs
new file mode 100644
index 00000000000000..c85d9bc26048f5
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs
@@ -0,0 +1,352 @@
+﻿// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.Extensions.Hosting.Tests
+{
+    public partial class LifecycleTests
+    {
+        [Fact]
+        public async Task StoppingConcurrently()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services
+                    .AddHostedService<StoppingTestClass<Impl1>>()
+                    .AddHostedService<StoppingTestClass<Impl2>>();
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                StoppingTestClass<Impl1>.s_wait1.Wait();
+                StoppingTestClass<Impl2>.s_wait1.Wait();
+                StoppingTestClass<Impl1>.s_wait2.Wait();
+                StoppingTestClass<Impl2>.s_wait2.Wait();
+                StoppingTestClass<Impl1>.s_wait3.Wait();
+                StoppingTestClass<Impl2>.s_wait3.Wait();
+
+                await host.StartAsync();
+                Verify(0, 0, 0, 0, 0, 0);
+
+                // Both run serially until the await.
+                Task stop = host.StopAsync();
+                Verify(1, 1, 0, 0, 0, 0);
+                await Task.Delay(s_superShortDelay);
+                Verify(1, 1, 0, 0, 0, 0);
+
+                // Resume and check that both are not finished.
+                StoppingTestClass<Impl1>.s_wait1.Release();
+                await StoppingTestClass<Impl1>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 0, 0, 0);
+
+                StoppingTestClass<Impl2>.s_wait1.Release();
+                await StoppingTestClass<Impl2>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 1, 0, 0);
+
+                // Resume and verify they finish.
+                StoppingTestClass<Impl1>.s_wait3.Release();
+                StoppingTestClass<Impl2>.s_wait3.Release();
+                await stop;
+                Verify(1, 1, 1, 1, 1, 1);
+                Assert.True(stop.IsCompleted);
+            }
+
+            void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2)
+            {
+                Assert.Equal(initial1, StoppingTestClass<Impl1>.s_initialCount);
+                Assert.Equal(initial2, StoppingTestClass<Impl2>.s_initialCount);
+                Assert.Equal(pause1, StoppingTestClass<Impl1>.s_pauseCount);
+                Assert.Equal(pause2, StoppingTestClass<Impl2>.s_pauseCount);
+                Assert.Equal(final1, StoppingTestClass<Impl1>.s_finalCount);
+                Assert.Equal(final2, StoppingTestClass<Impl2>.s_finalCount);
+            }
+        }
+
+        private class StoppingTestClass<T> : IHostedLifecycleService
+        {
+            public static int s_initialCount = 0;
+            public static int s_pauseCount = 0;
+            public static int s_finalCount = 0;
+            public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1);
+
+            public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public async Task StoppingAsync(CancellationToken cancellationToken)
+            {
+                s_initialCount++;
+                await s_wait1.WaitAsync();
+                s_pauseCount++;
+                s_wait2.Release();
+                await s_wait3.WaitAsync();
+                s_finalCount++;
+            }
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+
+        [Fact]
+        public async Task StopConcurrently()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.
+                    AddHostedService<StopTestClass<Impl1>>().
+                    AddHostedService<StopTestClass<Impl2>>().
+                    Configure<HostOptions>(opts => opts.ServicesStopConcurrently = true);
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                StopTestClass<Impl1>.s_wait1.Wait();
+                StopTestClass<Impl2>.s_wait1.Wait();
+                StopTestClass<Impl1>.s_wait2.Wait();
+                StopTestClass<Impl2>.s_wait2.Wait();
+                StopTestClass<Impl1>.s_wait3.Wait();
+                StopTestClass<Impl2>.s_wait3.Wait();
+
+                await host.StartAsync();
+                Verify(0, 0, 0, 0, 0, 0);
+
+                // Both run serially until the await.
+                Task stop = host.StopAsync();
+                Verify(1, 1, 0, 0, 0, 0);
+                await Task.Delay(s_superShortDelay);
+                Verify(1, 1, 0, 0, 0, 0);
+
+                // Resume and check that both are not finished.
+                StopTestClass<Impl1>.s_wait1.Release();
+                await StopTestClass<Impl1>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 0, 0, 0);
+
+                StopTestClass<Impl2>.s_wait1.Release();
+                await StopTestClass<Impl2>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 1, 0, 0);
+
+                // Resume and verify they finish.
+                StopTestClass<Impl1>.s_wait3.Release();
+                StopTestClass<Impl2>.s_wait3.Release();
+                await stop;
+                Verify(1, 1, 1, 1, 1, 1);
+                Assert.True(stop.IsCompleted);
+            }
+
+            void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2)
+            {
+                Assert.Equal(initial1, StopTestClass<Impl1>.s_initialCount);
+                Assert.Equal(initial2, StopTestClass<Impl2>.s_initialCount);
+                Assert.Equal(pause1, StopTestClass<Impl1>.s_pauseCount);
+                Assert.Equal(pause2, StopTestClass<Impl2>.s_pauseCount);
+                Assert.Equal(final1, StopTestClass<Impl1>.s_finalCount);
+                Assert.Equal(final2, StopTestClass<Impl2>.s_finalCount);
+            }
+        }
+
+        private class StopTestClass<T> : IHostedLifecycleService
+        {
+            public static int s_initialCount = 0;
+            public static int s_pauseCount = 0;
+            public static int s_finalCount = 0;
+            public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1);
+
+            public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+            public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public async Task StopAsync(CancellationToken cancellationToken)
+            {
+                s_initialCount++;
+                await s_wait1.WaitAsync();
+                s_pauseCount++;
+                s_wait2.Release();
+                await s_wait3.WaitAsync();
+                s_finalCount++;
+            }
+            public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+
+        [Fact]
+        public async Task StopNonconcurrently()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.
+                    AddHostedService<StopNonconcurrentTestClass<Impl1>>().
+                    AddHostedService<StopNonconcurrentTestClass<Impl2>>().
+                    Configure<HostOptions>(opts => opts.ServicesStopConcurrently = false);
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                StopNonconcurrentTestClass<Impl1>.s_wait.Wait();
+                StopNonconcurrentTestClass<Impl2>.s_wait.Wait();
+
+                await host.StartAsync();
+                Verify(0, 0);
+
+                // Both run serially in reverse order.
+                Task stop = host.StopAsync();
+                Verify(0, 1);
+                await Task.Delay(s_superShortDelay);
+                Verify(0, 1);
+
+                // Resume and verify they finish.
+                StopNonconcurrentTestClass<Impl1>.s_wait.Release();
+                StopNonconcurrentTestClass<Impl2>.s_wait.Release();
+                await stop;
+                Verify(1, 1);
+                Assert.True(stop.IsCompleted);
+            }
+
+            void Verify(int count1, int count2)
+            {
+                Assert.Equal(count1, StopNonconcurrentTestClass<Impl1>.s_count);
+                Assert.Equal(count2, StopNonconcurrentTestClass<Impl2>.s_count);
+            }
+        }
+
+        private class StopNonconcurrentTestClass<T> : IHostedLifecycleService
+        {
+            public static int s_count = 0;
+            public static SemaphoreSlim? s_wait = new SemaphoreSlim(1);
+
+            public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public async Task StopAsync(CancellationToken cancellationToken)
+            {
+                s_count++;
+                await s_wait.WaitAsync();
+            }
+            public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+
+        [Fact]
+        public async Task StoppedConcurrently()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services
+                    .AddHostedService<StoppedTestClass<Impl1>>()
+                    .AddHostedService<StoppedTestClass<Impl2>>();
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                StoppedTestClass<Impl1>.s_wait1.Wait();
+                StoppedTestClass<Impl2>.s_wait1.Wait();
+                StoppedTestClass<Impl1>.s_wait2.Wait();
+                StoppedTestClass<Impl2>.s_wait2.Wait();
+                StoppedTestClass<Impl1>.s_wait3.Wait();
+                StoppedTestClass<Impl2>.s_wait3.Wait();
+
+                await host.StartAsync();
+                Verify(0, 0, 0, 0, 0, 0);
+
+                // Both run serially until the await.
+                Task stop = host.StopAsync();
+                Verify(1, 1, 0, 0, 0, 0);
+                await Task.Delay(s_superShortDelay);
+                Verify(1, 1, 0, 0, 0, 0);
+
+                // Resume and check that both are not finished.
+                StoppedTestClass<Impl1>.s_wait1.Release();
+                await StoppedTestClass<Impl1>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 0, 0, 0);
+
+                StoppedTestClass<Impl2>.s_wait1.Release();
+                await StoppedTestClass<Impl2>.s_wait2.WaitAsync();
+                Verify(1, 1, 1, 1, 0, 0);
+
+                // Resume and verify they finish.
+                StoppedTestClass<Impl1>.s_wait3.Release();
+                StoppedTestClass<Impl2>.s_wait3.Release();
+                await stop;
+                Verify(1, 1, 1, 1, 1, 1);
+                Assert.True(stop.IsCompleted);
+            }
+
+            void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2)
+            {
+                Assert.Equal(initial1, StoppedTestClass<Impl1>.s_initialCount);
+                Assert.Equal(initial2, StoppedTestClass<Impl2>.s_initialCount);
+                Assert.Equal(pause1, StoppedTestClass<Impl1>.s_pauseCount);
+                Assert.Equal(pause2, StoppedTestClass<Impl2>.s_pauseCount);
+                Assert.Equal(final1, StoppedTestClass<Impl1>.s_finalCount);
+                Assert.Equal(final2, StoppedTestClass<Impl2>.s_finalCount);
+            }
+        }
+
+        private class StoppedTestClass<T> : IHostedLifecycleService
+        {
+            public static int s_initialCount = 0;
+            public static int s_pauseCount = 0;
+            public static int s_finalCount = 0;
+            public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1);
+            public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1);
+
+            public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public async Task StoppedAsync(CancellationToken cancellationToken)
+            {
+                s_initialCount++;
+                await s_wait1.WaitAsync();
+                s_pauseCount++;
+                s_wait2.Release();
+                await s_wait3.WaitAsync();
+                s_finalCount++;
+            }
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task StopPhasesException(bool throwAfterAsyncCall)
+        {
+            ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, false, false, false, true, true, true);
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.AddHostedService((token) => impl);
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                await host.StartAsync();
+                AggregateException ex = await Assert.ThrowsAnyAsync<AggregateException>(async () => await host.StopAsync());
+
+                Assert.True(impl.StartingCalled);
+                Assert.True(impl.StartCalled);
+                Assert.True(impl.StartedCalled);
+
+                // An exception during a stop phase does not prevent the next ones from running.
+                Assert.True(impl.StoppingCalled);
+                Assert.True(impl.StopCalled);
+                Assert.True(impl.StoppedCalled);
+
+                Assert.Contains("(ThrowOnStopping)", ex.Message);
+                Assert.Contains("(ThrowOnStop)", ex.Message);
+                Assert.Contains("(ThrowOnStopped)", ex.Message);
+
+                Assert.Equal(3, ex.InnerExceptions.Count);
+                Assert.Contains("(ThrowOnStopping)", ex.InnerExceptions[0].Message);
+                Assert.Contains("(ThrowOnStop)", ex.InnerExceptions[1].Message);
+                Assert.Contains("(ThrowOnStopped)", ex.InnerExceptions[2].Message);
+            }
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Timeouts.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Timeouts.cs
new file mode 100644
index 00000000000000..bf215b490f44dc
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Timeouts.cs
@@ -0,0 +1,169 @@
+﻿// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.Extensions.Hosting.Tests
+{
+    public partial class LifecycleTests
+    {
+        [OuterLoop("Uses Task.Delay")]
+        [Theory]
+        [InlineData(TimeoutService.Phase.Starting)]
+        [InlineData(TimeoutService.Phase.Start)]
+        [InlineData(TimeoutService.Phase.Started)]
+        public async Task StartTimeoutClass_WithValue(TimeoutService.Phase phase)
+        {
+            var host = new HostBuilder()
+                 .ConfigureServices((hostContext, services) =>
+                 {
+                     services.AddHostedService((token) => new TimeoutService(phase));
+                     services.Configure<HostOptions>((opts) =>
+                     {
+                         opts.StartupTimeout = s_shortDelay;
+                     });
+                 })
+                 .UseConsoleLifetime()
+                 .Build();
+
+            await Assert.ThrowsAsync<TaskCanceledException>(async () => await host.StartAsync());
+        }
+
+        [OuterLoop("Uses Task.Delay")]
+        [Theory]
+        [InlineData(TimeoutService.Phase.Start)]
+        public async Task StartTimeoutClass_WithValue_Concurrently(TimeoutService.Phase phase)
+        {
+            var host = new HostBuilder()
+                 .ConfigureServices((hostContext, services) =>
+                 {
+                     services.AddHostedService((token) => new TimeoutService(phase));
+                     services.Configure<HostOptions>((opts) =>
+                     {
+                         opts.StartupTimeout = s_shortDelay;
+                         opts.ServicesStartConcurrently = true;
+                     });
+                 })
+                 .UseConsoleLifetime()
+                 .Build();
+
+            await Assert.ThrowsAsync<TaskCanceledException>(async () => await host.StartAsync());
+        }
+
+        [OuterLoop("Uses Task.Delay")]
+        [Theory]
+        [InlineData(TimeoutService.Phase.Stopping)]
+        [InlineData(TimeoutService.Phase.Stop)]
+        [InlineData(TimeoutService.Phase.Stopped)]
+        public async Task StopTimeoutClass_WithValue(TimeoutService.Phase phase)
+        {
+            var host = new HostBuilder()
+                 .ConfigureServices((hostContext, services) =>
+                 {
+                     services.AddHostedService((token) => new TimeoutService(phase));
+                     services.Configure<HostOptions>((opts) =>
+                     {
+                         opts.ShutdownTimeout = s_shortDelay;
+                     });
+                 })
+                 .UseConsoleLifetime()
+                 .Build();
+
+            await host.StartAsync();
+            await Assert.ThrowsAsync<TaskCanceledException>(async () => await host.StopAsync());
+        }
+
+        [OuterLoop("Uses Task.Delay")]
+        [Theory]
+        [InlineData(TimeoutService.Phase.Stop)]
+        public async Task StopTimeoutClass_WithValue_Concurrently(TimeoutService.Phase phase)
+        {
+            var host = new HostBuilder()
+                 .ConfigureServices((hostContext, services) =>
+                 {
+                     services.AddHostedService((token) => new TimeoutService(phase));
+                     services.Configure<HostOptions>((opts) =>
+                     {
+                         opts.ShutdownTimeout = s_shortDelay;
+                         opts.ServicesStopConcurrently = true;
+                     });
+                 })
+                 .UseConsoleLifetime()
+                 .Build();
+
+            await host.StartAsync();
+            await Assert.ThrowsAsync<TaskCanceledException>(async () => await host.StopAsync());
+        }
+
+        public class TimeoutService : IHostedLifecycleService
+        {
+            private Phase _phase;
+
+            public TimeoutService(Phase phase)
+            {
+                _phase = phase;
+            }
+
+            public async Task StartingAsync(CancellationToken cancellationToken)
+            {
+                if (_phase == Phase.Starting)
+                {
+                    await Task.Delay(s_longDelay, cancellationToken);
+                }
+            }
+
+            public async Task StartAsync(CancellationToken cancellationToken)
+            {
+                if (_phase == Phase.Start)
+                {
+                    await Task.Delay(s_longDelay, cancellationToken);
+                }
+            }
+
+            public async Task StartedAsync(CancellationToken cancellationToken)
+            {
+                if (_phase == Phase.Started)
+                {
+                    await Task.Delay(s_longDelay, cancellationToken);
+                }
+            }
+
+            public async Task StoppingAsync(CancellationToken cancellationToken)
+            {
+                if (_phase == Phase.Stopping)
+                {
+                    await Task.Delay(s_longDelay, cancellationToken);
+                }
+            }
+
+            public async Task StopAsync(CancellationToken cancellationToken)
+            {
+                if (_phase == Phase.Stop)
+                {
+                    await Task.Delay(s_longDelay, cancellationToken);
+                }
+            }
+
+            public async Task StoppedAsync(CancellationToken cancellationToken)
+            {
+                if (_phase == Phase.Stopped)
+                {
+                    await Task.Delay(s_longDelay, cancellationToken);
+                }
+            }
+
+            public enum Phase
+            {
+                Starting,
+                Start,
+                Started,
+                Stopping,
+                Stop,
+                Stopped
+            }
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs
new file mode 100644
index 00000000000000..518f8b364bb89b
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs
@@ -0,0 +1,177 @@
+﻿// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.Extensions.Hosting.Tests
+{
+    public partial class LifecycleTests
+    {
+        private static TimeSpan s_superShortDelay = TimeSpan.FromSeconds(.05);
+
+        // Tests that actually delay this long should be [OuterLoop]:
+        private static TimeSpan s_shortDelay = TimeSpan.FromSeconds(.5);
+        private static TimeSpan s_longDelay = TimeSpan.FromSeconds(5); 
+
+        public static IHostBuilder CreateHostBuilder(Action<IServiceCollection> configure) =>
+            new HostBuilder().ConfigureServices(configure);
+
+        [Fact]
+        public async Task HostedService_CallbackOccursOnce()
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services.AddHostedService<HostedService_CallbackOccursOnce_Impl>();
+                services.AddHostedService<HostedService_CallbackOccursOnce_Impl>();
+            });
+
+            using (IHost host = hostBuilder.Build())
+            {
+                await host.StartAsync();
+            }
+
+            Assert.Equal(1, HostedService_CallbackOccursOnce_Impl.s_callbackCallCount);
+        }
+
+        private class HostedService_CallbackOccursOnce_Impl : IHostedService
+        {
+            public static int s_callbackCallCount = 0;
+
+            public Task StartAsync(CancellationToken cancellationToken)
+            {
+                s_callbackCallCount++;
+                return Task.CompletedTask;
+            }
+
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+
+        private class ExceptionImpl : IHostedLifecycleService
+        {
+            private bool _throwAfterAsyncCall;
+            public bool StartingCalled = false;
+            public bool StartCalled = false;
+            public bool StartedCalled = false;
+            public bool StoppingCalled = false;
+            public bool StopCalled = false;
+            public bool StoppedCalled = false;
+
+            public bool ThrowOnStarting;
+            public bool ThrowOnStart;
+            public bool ThrowOnStarted;
+            public bool ThrowOnStopping;
+            public bool ThrowOnStop;
+            public bool ThrowOnStopped;
+
+            public ExceptionImpl(
+                bool throwAfterAsyncCall,
+                bool throwOnStarting,
+                bool throwOnStart,
+                bool throwOnStarted,
+                bool throwOnStopping,
+                bool throwOnStop,
+                bool throwOnStopped)
+            {
+                _throwAfterAsyncCall = throwAfterAsyncCall;
+                ThrowOnStarting = throwOnStarting;
+                ThrowOnStart = throwOnStart;
+                ThrowOnStarted = throwOnStarted;
+                ThrowOnStopping = throwOnStopping;
+                ThrowOnStop = throwOnStop;
+                ThrowOnStopped = throwOnStopped;
+            }
+
+            public async Task StartingAsync(CancellationToken cancellationToken)
+            {
+                StartingCalled = true;
+                if (ThrowOnStarting)
+                {
+                    if (_throwAfterAsyncCall)
+                    {
+                        await Task.Delay(s_superShortDelay);
+                    }
+
+                    throw new Exception("(ThrowOnStarting)");
+                }
+            }
+
+            public async Task StartAsync(CancellationToken cancellationToken)
+            {
+                StartCalled = true;
+                if (ThrowOnStart)
+                {
+                    if (_throwAfterAsyncCall)
+                    {
+                        await Task.Delay(s_superShortDelay);
+                    }
+
+                    throw new Exception("(ThrowOnStart)");
+                }
+            }
+
+            public async Task StartedAsync(CancellationToken cancellationToken)
+            {
+                StartedCalled = true;
+                if (ThrowOnStarted)
+                {
+                    if (_throwAfterAsyncCall)
+                    {
+                        await Task.Delay(s_superShortDelay);
+                    }
+
+                    throw new Exception("(ThrowOnStarted)");
+                }
+            }
+
+            public async Task StoppingAsync(CancellationToken cancellationToken)
+            {
+                StoppingCalled = true;
+                if (ThrowOnStopping)
+                {
+                    if (_throwAfterAsyncCall)
+                    {
+                        await Task.Delay(s_superShortDelay);
+                    }
+
+                    throw new Exception("(ThrowOnStopping)");
+                }
+            }
+
+            public async Task StopAsync(CancellationToken cancellationToken)
+            {
+                StopCalled = true;
+                if (ThrowOnStop)
+                {
+                    if (_throwAfterAsyncCall)
+                    {
+                        await Task.Delay(s_superShortDelay);
+                    }
+
+                    throw new Exception("(ThrowOnStop)");
+                }
+            }
+
+            public async Task StoppedAsync(CancellationToken cancellationToken)
+            {
+                StoppedCalled = true;
+                if (ThrowOnStopped)
+                {
+                    if (_throwAfterAsyncCall)
+                    {
+                        await Task.Delay(s_superShortDelay);
+                    }
+
+                    throw new Exception("(ThrowOnStopped)");
+                }
+            }
+        }
+
+        // These are used to close open generic types:
+        private sealed class Impl1 { }
+        private sealed class Impl2 { }
+    }
+}

From 73d5854cb304a11e152e6a3b4217cf969ceffa1f Mon Sep 17 00:00:00 2001
From: Steve Harter <steveharter@users.noreply.github.com>
Date: Thu, 29 Jun 2023 11:23:47 -0500
Subject: [PATCH 2/5] Use Services<Start|Stop>Concurrently knobs for new
 callbacks

---
 .../src/Internal/Host.cs                      | 270 ++++++++++++++----
 .../tests/UnitTests/LifecycleTests.Start.cs   |   8 +-
 .../tests/UnitTests/LifecycleTests.Stop.cs    |   8 +-
 .../tests/UnitTests/LifecycleTests.cs         | 139 +++++++--
 4 files changed, 338 insertions(+), 87 deletions(-)

diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
index 2f98128525e966..9623fb333e04d0 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
@@ -55,6 +55,13 @@ public Host(IServiceProvider services,
 
         public IServiceProvider Services { get; }
 
+        /// <summary>
+        /// Order:
+        ///  IHostedLifecycleService.StartingAsync
+        ///  IHostedService.Start
+        ///  IHostedLifecycleService.StartedAsync
+        ///  IHostApplicationLifetime.ApplicationStarted
+        /// </summary>
         public async Task StartAsync(CancellationToken cancellationToken = default)
         {
             _logger.Starting();
@@ -82,18 +89,39 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
                 await _hostLifetime.WaitForStartAsync(token).ConfigureAwait(false);
                 token.ThrowIfCancellationRequested();
 
-                if (_hostedLifecycleServices is not null)
-                {
-                    await CallLifeCycle_Starting(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false);
-                }
-
                 // Run IHostedService.Start either concurrently or serially.
                 if (_options.ServicesStartConcurrently)
                 {
+                    if (_hostedLifecycleServices is not null)
+                    {
+                        await CallLifeCycle_Starting(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false);
+                    }
+
                     await CallLifeCycle_Start(_hostedServices, token, exceptions).ConfigureAwait(false);
+
+                    if (_hostedLifecycleServices is not null)
+                    {
+                        await CallLifeCycle_Started(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false);
+                    }
                 }
                 else
                 {
+                    if (_hostedLifecycleServices is not null)
+                    {
+                        foreach (IHostedLifecycleService hostedService in _hostedLifecycleServices)
+                        {
+                            try
+                            {
+                                await hostedService.StartingAsync(token).ConfigureAwait(false);
+                            }
+                            catch (Exception ex)
+                            {
+                                exceptions.Add(ex);
+                                break;
+                            }
+                        }
+                    }
+
                     foreach (IHostedService hostedService in _hostedServices)
                     {
                         try
@@ -106,11 +134,22 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
                             break;
                         }
                     }
-                }
 
-                if (_hostedLifecycleServices is not null)
-                {
-                    await CallLifeCycle_Started(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false);
+                    if (_hostedLifecycleServices is not null)
+                    {
+                        foreach (IHostedLifecycleService hostedService in _hostedLifecycleServices)
+                        {
+                            try
+                            {
+                                await hostedService.StartedAsync(token).ConfigureAwait(false);
+                            }
+                            catch (Exception ex)
+                            {
+                                exceptions.Add(ex);
+                                break;
+                            }
+                        }
+                    }
                 }
 
                 if (exceptions.Count > 0)
@@ -151,7 +190,7 @@ private async Task TryExecuteBackgroundServiceAsync(BackgroundService background
         {
             // backgroundService.ExecuteTask may not be set (e.g. if the derived class doesn't call base.StartAsync)
             Task? backgroundTask = backgroundService.ExecuteTask;
-            if (backgroundTask == null)
+            if (backgroundTask is null)
             {
                 return;
             }
@@ -178,6 +217,14 @@ private async Task TryExecuteBackgroundServiceAsync(BackgroundService background
             }
         }
 
+        /// <summary>
+        /// Order:
+        ///  IHostedLifecycleService.StoppingAsync
+        ///  IHostApplicationLifetime.ApplicationStopping
+        ///  IHostedService.Stop
+        ///  IHostedLifecycleService.StoppedAsync
+        ///  IHostApplicationLifetime.ApplicationStopped
+        /// </summary>
         public async Task StopAsync(CancellationToken cancellationToken = default)
         {
             _stopCalled = true;
@@ -201,27 +248,70 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
                 CancellationToken token = linkedCts.Token;
                 List<Exception> exceptions = new();
 
-                // Trigger IHostApplicationLifetime.ApplicationStopping
-                _applicationLifetime.StopApplication();
-
-                if (_hostedServices != null) // Started?
+                if (_hostedServices is null)
+                {
+                    // Trigger IHostApplicationLifetime.ApplicationStopping even if StartAsync wasn't called.
+                    _applicationLifetime.StopApplication();
+                }
+                else
                 {
                     // Ensure hosted services are stopped in LIFO order
                     IEnumerable<IHostedService> reversedServices = _hostedServices.Reverse();
                     IEnumerable<IHostedLifecycleService>? reversedLifetimeServices = _hostedLifecycleServices?.Reverse();
 
-                    if (reversedLifetimeServices is not null)
-                    {
-                        await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false);
-                    }
-
-                    // Run IHostedService.Stop either concurrently or serially.
                     if (_options.ServicesStopConcurrently)
                     {
+                        if (reversedLifetimeServices is not null)
+                        {
+                            await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false);
+                        }
+
+                        try
+                        {
+                            // Trigger IHostApplicationLifetime.ApplicationStopping.
+                            _applicationLifetime.StopApplication();
+                        }
+                        catch (Exception ex)
+                        {
+                            LogExceptions(); // Exceptions thrown from StoppingAsync().
+                            throw ex;
+                        }
+
                         await CallLifeCycle_Stop(reversedServices, token, exceptions).ConfigureAwait(false);
+
+                        if (reversedLifetimeServices is not null)
+                        {
+                            await CallLifeCycle_Stopped(reversedLifetimeServices, token, exceptions).ConfigureAwait(false);
+                        }
                     }
                     else
                     {
+                        if (reversedLifetimeServices is not null)
+                        {
+                            foreach (IHostedLifecycleService hostedService in reversedLifetimeServices)
+                            {
+                                try
+                                {
+                                    await hostedService.StoppingAsync(token).ConfigureAwait(false);
+                                }
+                                catch (Exception ex)
+                                {
+                                    exceptions.Add(ex);
+                                }
+                            }
+                        }
+
+                        try
+                        {
+                            // Trigger IHostApplicationLifetime.ApplicationStopping.
+                            _applicationLifetime.StopApplication();
+                        }
+                        catch (Exception ex)
+                        {
+                            LogExceptions(); // Exceptions thrown from StoppingAsync().
+                            throw ex;
+                        }
+
                         foreach (IHostedService hostedService in reversedServices)
                         {
                             try
@@ -233,40 +323,46 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
                                 exceptions.Add(ex);
                             }
                         }
-                    }
 
-                    if (reversedLifetimeServices is not null)
-                    {
-                        await CallLifeCycle_Stopped(reversedLifetimeServices, token, exceptions).ConfigureAwait(false);
+                        if (reversedLifetimeServices is not null)
+                        {
+                            foreach (IHostedLifecycleService hostedService in reversedLifetimeServices)
+                            {
+                                try
+                                {
+                                    await hostedService.StoppedAsync(token).ConfigureAwait(false);
+                                }
+                                catch (Exception ex)
+                                {
+                                    exceptions.Add(ex);
+                                }
+                            }
+                        }
                     }
                 }
 
+                LogExceptions();
+
                 // Fire IHostApplicationLifetime.Stopped
                 _applicationLifetime.NotifyStopped();
 
-                try
-                {
-                    await _hostLifetime.StopAsync(token).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.Add(ex);
-                }
-
-                if (exceptions.Count > 0)
+                void LogExceptions()
                 {
-                    if (exceptions.Count == 1)
+                    if (exceptions.Count > 0)
                     {
-                        // 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.", exceptions);
-                        _logger.StoppedWithException(ex);
-                        throw ex;
+                        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.", exceptions);
+                            _logger.StoppedWithException(ex);
+                            throw ex;
+                        }
                     }
                 }
             }
@@ -286,12 +382,22 @@ private static async Task CallLifeCycle_Starting(IEnumerable<IHostedLifecycleSer
 
             foreach (IHostedLifecycleService service in services)
             {
-                Task? task =  task = service.StartingAsync(token);
+                Task task;
+                try
+                {
+                    task = service.StartingAsync(token);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.Add(ex); // Log exception from sync method.
+                    continue;
+                }
+
                 if (task.IsCompleted)
                 {
                     if (task.Exception is not null)
                     {
-                        exceptions.Add(task.Exception);
+                        exceptions.Add(task.Exception); // Log exception from async method.
                     }
                 }
                 else
@@ -328,12 +434,22 @@ private static async Task CallLifeCycle_Start(IEnumerable<IHostedService> servic
 
             foreach (IHostedService service in services)
             {
-                Task? task = service.StartAsync(token);
+                Task task;
+                try
+                {
+                    task = service.StartAsync(token);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.Add(ex); // Log exception from sync method.
+                    continue;
+                }
+
                 if (task.IsCompleted)
                 {
                     if (task.Exception is not null)
                     {
-                        exceptions.Add(task.Exception);
+                        exceptions.Add(task.Exception); // Log exception from async method.
                     }
                 }
                 else
@@ -370,12 +486,22 @@ private static async Task CallLifeCycle_Started(IEnumerable<IHostedLifecycleServ
 
             foreach (IHostedLifecycleService service in services)
             {
-                Task? task = task = service.StartedAsync(token);
+                Task task;
+                try
+                {
+                    task = service.StartedAsync(token);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.Add(ex); // Log exception from sync method.
+                    continue;
+                }
+
                 if (task.IsCompleted)
                 {
                     if (task.Exception is not null)
                     {
-                        exceptions.Add(task.Exception);
+                        exceptions.Add(task.Exception); // Log exception from async method.
                     }
                 }
                 else
@@ -413,12 +539,22 @@ private static async Task CallLifeCycle_Stopping(IEnumerable<IHostedLifecycleSer
 
             foreach (IHostedLifecycleService service in services)
             {
-                Task? task = task = service.StoppingAsync(token);
+                Task task;
+                try
+                {
+                    task = service.StoppingAsync(token);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.Add(ex); // Log exception from sync method.
+                    continue;
+                }
+
                 if (task.IsCompleted)
                 {
                     if (task.Exception is not null)
                     {
-                        exceptions.Add(task.Exception);
+                        exceptions.Add(task.Exception); // Log exception from async method.
                     }
                 }
                 else
@@ -455,12 +591,22 @@ private static async Task CallLifeCycle_Stop(IEnumerable<IHostedService> service
 
             foreach (IHostedService service in services)
             {
-                Task? task = task = service.StopAsync(token);
+                Task task;
+                try
+                {
+                    task = service.StopAsync(token);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.Add(ex); // Log exception from sync method.
+                    continue;
+                }
+
                 if (task.IsCompleted)
                 {
                     if (task.Exception is not null)
                     {
-                        exceptions.Add(task.Exception);
+                        exceptions.Add(task.Exception); // Log exception from async method.
                     }
                 }
                 else
@@ -497,12 +643,22 @@ private static async Task CallLifeCycle_Stopped(IEnumerable<IHostedLifecycleServ
 
             foreach (IHostedLifecycleService service in services)
             {
-                Task? task = task = service.StoppedAsync(token);
+                Task task;
+                try
+                {
+                    task = service.StoppedAsync(token);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.Add(ex); // Log exception from sync method.
+                    continue;
+                }
+
                 if (task.IsCompleted)
                 {
                     if (task.Exception is not null)
                     {
-                        exceptions.Add(task.Exception);
+                        exceptions.Add(task.Exception); // Log exception from async method.
                     }
                 }
                 else
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs
index 61d5ce9583c651..1de8c2b9c494fd 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs
@@ -18,7 +18,8 @@ public async Task StartingConcurrently()
             {
                 services
                     .AddHostedService<StartingTestClass<Impl1>>()
-                    .AddHostedService<StartingTestClass<Impl2>>();
+                    .AddHostedService<StartingTestClass<Impl2>>()
+                    .Configure<HostOptions>(opts => opts.ServicesStartConcurrently = true);
             });
 
             using (IHost host = hostBuilder.Build())
@@ -235,7 +236,8 @@ public async Task StartedConcurrently()
             {
                 services
                     .AddHostedService<StartedTestClass<Impl1>>()
-                    .AddHostedService<StartedTestClass<Impl2>>();
+                    .AddHostedService<StartedTestClass<Impl2>>()
+                    .Configure<HostOptions>(opts => opts.ServicesStartConcurrently = true);
             });
 
             using (IHost host = hostBuilder.Build())
@@ -313,7 +315,7 @@ public async Task StartedAsync(CancellationToken cancellationToken)
         [InlineData(false)]
         public async Task StartPhasesException(bool throwAfterAsyncCall)
         {
-            ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, true, true, true, false, false, false);
+            ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, throwOnStartup: true, throwOnShutdown: false);
             var hostBuilder = CreateHostBuilder(services =>
             {
                 services.AddHostedService((token) => impl);
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs
index c85d9bc26048f5..03b684df4087bf 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs
@@ -18,7 +18,8 @@ public async Task StoppingConcurrently()
             {
                 services
                     .AddHostedService<StoppingTestClass<Impl1>>()
-                    .AddHostedService<StoppingTestClass<Impl2>>();
+                    .AddHostedService<StoppingTestClass<Impl2>>()
+                    .Configure<HostOptions>(opts => opts.ServicesStopConcurrently = true);
             });
 
             using (IHost host = hostBuilder.Build())
@@ -239,7 +240,8 @@ public async Task StoppedConcurrently()
             {
                 services
                     .AddHostedService<StoppedTestClass<Impl1>>()
-                    .AddHostedService<StoppedTestClass<Impl2>>();
+                    .AddHostedService<StoppedTestClass<Impl2>>()
+                    .Configure<HostOptions>(opts => opts.ServicesStopConcurrently = true);
             });
 
             using (IHost host = hostBuilder.Build())
@@ -318,7 +320,7 @@ public async Task StoppedAsync(CancellationToken cancellationToken)
         [InlineData(false)]
         public async Task StopPhasesException(bool throwAfterAsyncCall)
         {
-            ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, false, false, false, true, true, true);
+            ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, throwOnStartup: false, throwOnShutdown: true);
             var hostBuilder = CreateHostBuilder(services =>
             {
                 services.AddHostedService((token) => impl);
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs
index 518f8b364bb89b..3737cdac69d83e 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.DependencyInjection;
@@ -50,6 +51,108 @@ public Task StartAsync(CancellationToken cancellationToken)
             public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
         }
 
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task CallbackOrder(bool concurrently)
+        {
+            var hostBuilder = CreateHostBuilder(services =>
+            {
+                services
+                    .AddHostedService<CallbackOrder_Impl>()
+                    .AddSingleton((sp) => sp.GetServices<IHostedService>().OfType<CallbackOrder_Impl>().First())
+                    .Configure<HostOptions>(opts => opts.ServicesStartConcurrently = concurrently)
+                    .Configure<HostOptions>(opts => opts.ServicesStopConcurrently = concurrently);
+            });
+            using (IHost host = hostBuilder.Build())
+            {
+                CallbackOrder_Impl impl = host.Services.GetService<CallbackOrder_Impl>();
+
+                await host.StartAsync();
+                await host.StopAsync();
+
+                Assert.Equal(1, impl._startingOrder);
+                Assert.Equal(2, impl._startOrder);
+                Assert.Equal(3, impl._startedOrder);
+                Assert.Equal(4, impl._applicationStartedOrder);
+                Assert.Equal(5, impl._stoppingOrder);
+                Assert.Equal(6, impl._applicationStoppingOrder);
+                Assert.Equal(7, impl._stopOrder);
+                Assert.Equal(8, impl._stoppedOrder);
+                Assert.Equal(9, impl._applicationStoppedOrder);
+            }
+        }
+
+        private class CallbackOrder_Impl : IHostedLifecycleService
+        {
+            public int _startingOrder;
+            public int _startOrder;
+            public int _startedOrder;
+            public int _applicationStartedOrder;
+            public int _stoppingOrder;
+            public int _applicationStoppingOrder;
+            public int _stopOrder;
+            public int _stoppedOrder;
+            public int _applicationStoppedOrder;
+
+            private int _callCount;
+
+            public CallbackOrder_Impl(IServiceProvider provider)
+            {
+                IHostApplicationLifetime lifetime = provider.GetService<IHostApplicationLifetime>();
+
+                lifetime.ApplicationStarted.Register(() =>
+                {
+                    _applicationStartedOrder = ++_callCount;
+                });
+
+                lifetime.ApplicationStopping.Register(() =>
+                {
+                    _applicationStoppingOrder = ++_callCount;
+                });
+
+                lifetime.ApplicationStopped.Register(() =>
+                {
+                    _applicationStoppedOrder = ++_callCount;
+                });
+            }
+
+            public Task StartingAsync(CancellationToken cancellationToken)
+            {
+                _startingOrder = ++_callCount;
+                return Task.CompletedTask;
+            }
+
+            public Task StartAsync(CancellationToken cancellationToken)
+            {
+                _startOrder = ++_callCount;
+                return Task.CompletedTask;
+            }
+
+            public Task StartedAsync(CancellationToken cancellationToken)
+            {
+                _startedOrder = ++_callCount;
+                return Task.CompletedTask;
+            }
+
+            public Task StoppingAsync(CancellationToken cancellationToken)
+            {
+                _stoppingOrder = ++_callCount;
+                return Task.CompletedTask;
+            }
+
+            public Task StopAsync(CancellationToken cancellationToken)
+            {
+                _stopOrder = ++_callCount;
+                return Task.CompletedTask;
+            }
+            public Task StoppedAsync(CancellationToken cancellationToken)
+            {
+                _stoppedOrder = ++_callCount;
+                return Task.CompletedTask;
+            }
+        }
+
         private class ExceptionImpl : IHostedLifecycleService
         {
             private bool _throwAfterAsyncCall;
@@ -60,35 +163,23 @@ private class ExceptionImpl : IHostedLifecycleService
             public bool StopCalled = false;
             public bool StoppedCalled = false;
 
-            public bool ThrowOnStarting;
-            public bool ThrowOnStart;
-            public bool ThrowOnStarted;
-            public bool ThrowOnStopping;
-            public bool ThrowOnStop;
-            public bool ThrowOnStopped;
+            public bool ThrowOnStartup;
+            public bool ThrowOnShutdown;
 
             public ExceptionImpl(
                 bool throwAfterAsyncCall,
-                bool throwOnStarting,
-                bool throwOnStart,
-                bool throwOnStarted,
-                bool throwOnStopping,
-                bool throwOnStop,
-                bool throwOnStopped)
+                bool throwOnStartup,
+                bool throwOnShutdown)
             {
                 _throwAfterAsyncCall = throwAfterAsyncCall;
-                ThrowOnStarting = throwOnStarting;
-                ThrowOnStart = throwOnStart;
-                ThrowOnStarted = throwOnStarted;
-                ThrowOnStopping = throwOnStopping;
-                ThrowOnStop = throwOnStop;
-                ThrowOnStopped = throwOnStopped;
+                ThrowOnStartup = throwOnStartup;
+                ThrowOnShutdown = throwOnShutdown;
             }
 
             public async Task StartingAsync(CancellationToken cancellationToken)
             {
                 StartingCalled = true;
-                if (ThrowOnStarting)
+                if (ThrowOnStartup)
                 {
                     if (_throwAfterAsyncCall)
                     {
@@ -102,7 +193,7 @@ public async Task StartingAsync(CancellationToken cancellationToken)
             public async Task StartAsync(CancellationToken cancellationToken)
             {
                 StartCalled = true;
-                if (ThrowOnStart)
+                if (ThrowOnStartup)
                 {
                     if (_throwAfterAsyncCall)
                     {
@@ -116,7 +207,7 @@ public async Task StartAsync(CancellationToken cancellationToken)
             public async Task StartedAsync(CancellationToken cancellationToken)
             {
                 StartedCalled = true;
-                if (ThrowOnStarted)
+                if (ThrowOnStartup)
                 {
                     if (_throwAfterAsyncCall)
                     {
@@ -130,7 +221,7 @@ public async Task StartedAsync(CancellationToken cancellationToken)
             public async Task StoppingAsync(CancellationToken cancellationToken)
             {
                 StoppingCalled = true;
-                if (ThrowOnStopping)
+                if (ThrowOnShutdown)
                 {
                     if (_throwAfterAsyncCall)
                     {
@@ -144,7 +235,7 @@ public async Task StoppingAsync(CancellationToken cancellationToken)
             public async Task StopAsync(CancellationToken cancellationToken)
             {
                 StopCalled = true;
-                if (ThrowOnStop)
+                if (ThrowOnShutdown)
                 {
                     if (_throwAfterAsyncCall)
                     {
@@ -158,7 +249,7 @@ public async Task StopAsync(CancellationToken cancellationToken)
             public async Task StoppedAsync(CancellationToken cancellationToken)
             {
                 StoppedCalled = true;
-                if (ThrowOnStopped)
+                if (ThrowOnShutdown)
                 {
                     if (_throwAfterAsyncCall)
                     {

From 39518bcfd1ea7af1a29e49f86fb885186f6513cc Mon Sep 17 00:00:00 2001
From: Steve Harter <steveharter@users.noreply.github.com>
Date: Thu, 29 Jun 2023 13:18:28 -0500
Subject: [PATCH 3/5] Fix issue with IHostLifetime

---
 .../src/Internal/Host.cs                      | 79 +++++++++----------
 .../tests/UnitTests/LifecycleTests.cs         | 37 ++++++---
 2 files changed, 66 insertions(+), 50 deletions(-)

diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
index 9623fb333e04d0..932d8d757df8c2 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
@@ -57,6 +57,7 @@ public Host(IServiceProvider services,
 
         /// <summary>
         /// Order:
+        ///  IHostLifetime.WaitForStartAsync
         ///  IHostedLifecycleService.StartingAsync
         ///  IHostedService.Start
         ///  IHostedLifecycleService.StartedAsync
@@ -82,13 +83,15 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
             using (linkedCts)
             {
                 CancellationToken token = linkedCts.Token;
-                List<Exception> exceptions = new();
-                _hostedServices = Services.GetRequiredService<IEnumerable<IHostedService>>();
-                _hostedLifecycleServices = GetHostLifecycles(_hostedServices);
 
+                // This may not catch exceptions.
                 await _hostLifetime.WaitForStartAsync(token).ConfigureAwait(false);
                 token.ThrowIfCancellationRequested();
 
+                List<Exception> exceptions = new();
+                _hostedServices = Services.GetRequiredService<IEnumerable<IHostedService>>();
+                _hostedLifecycleServices = GetHostLifecycles(_hostedServices);
+
                 // Run IHostedService.Start either concurrently or serially.
                 if (_options.ServicesStartConcurrently)
                 {
@@ -170,6 +173,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
                 }
 
                 // Fire IHostApplicationLifetime.Started
+                // This catches all exceptions and does not re-throw.
                 _applicationLifetime.NotifyStarted();
             }
 
@@ -212,6 +216,8 @@ private async Task TryExecuteBackgroundServiceAsync(BackgroundService background
                 if (_options.BackgroundServiceExceptionBehavior == BackgroundServiceExceptionBehavior.StopHost)
                 {
                     _logger.BackgroundServiceStoppingHost(ex);
+
+                    // This catches all exceptions and does not re-throw.
                     _applicationLifetime.StopApplication();
                 }
             }
@@ -224,6 +230,7 @@ private async Task TryExecuteBackgroundServiceAsync(BackgroundService background
         ///  IHostedService.Stop
         ///  IHostedLifecycleService.StoppedAsync
         ///  IHostApplicationLifetime.ApplicationStopped
+        ///  IHostLifetime.StopAsync
         /// </summary>
         public async Task StopAsync(CancellationToken cancellationToken = default)
         {
@@ -266,16 +273,9 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
                             await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false);
                         }
 
-                        try
-                        {
-                            // Trigger IHostApplicationLifetime.ApplicationStopping.
-                            _applicationLifetime.StopApplication();
-                        }
-                        catch (Exception ex)
-                        {
-                            LogExceptions(); // Exceptions thrown from StoppingAsync().
-                            throw ex;
-                        }
+                        // Trigger IHostApplicationLifetime.ApplicationStopping.
+                        // This catches all exceptions and does not re-throw.
+                        _applicationLifetime.StopApplication();
 
                         await CallLifeCycle_Stop(reversedServices, token, exceptions).ConfigureAwait(false);
 
@@ -301,16 +301,9 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
                             }
                         }
 
-                        try
-                        {
-                            // Trigger IHostApplicationLifetime.ApplicationStopping.
-                            _applicationLifetime.StopApplication();
-                        }
-                        catch (Exception ex)
-                        {
-                            LogExceptions(); // Exceptions thrown from StoppingAsync().
-                            throw ex;
-                        }
+                        // Trigger IHostApplicationLifetime.ApplicationStopping.
+                        // This catches all exceptions and does not re-throw.
+                        _applicationLifetime.StopApplication();
 
                         foreach (IHostedService hostedService in reversedServices)
                         {
@@ -341,28 +334,34 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
                     }
                 }
 
-                LogExceptions();
-
                 // Fire IHostApplicationLifetime.Stopped
+                // This catches all exceptions and does not re-throw.
                 _applicationLifetime.NotifyStopped();
 
-                void LogExceptions()
+                // This may not catch exceptions, so we do it here.
+                try
+                {
+                    await _hostLifetime.StopAsync(token).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    exceptions.Add(ex);
+                }
+
+                if (exceptions.Count > 0)
                 {
-                    if (exceptions.Count > 0)
+                    if (exceptions.Count == 1)
                     {
-                        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.", exceptions);
-                            _logger.StoppedWithException(ex);
-                            throw ex;
-                        }
+                        // 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.", exceptions);
+                        _logger.StoppedWithException(ex);
+                        throw ex;
                     }
                 }
             }
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs
index 3737cdac69d83e..373913cc3de6bf 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs
@@ -61,6 +61,7 @@ public async Task CallbackOrder(bool concurrently)
                 services
                     .AddHostedService<CallbackOrder_Impl>()
                     .AddSingleton((sp) => sp.GetServices<IHostedService>().OfType<CallbackOrder_Impl>().First())
+                    .AddSingleton<IHostLifetime>((sp) => sp.GetServices<IHostedService>().OfType<CallbackOrder_Impl>().First())
                     .Configure<HostOptions>(opts => opts.ServicesStartConcurrently = concurrently)
                     .Configure<HostOptions>(opts => opts.ServicesStopConcurrently = concurrently);
             });
@@ -71,20 +72,23 @@ public async Task CallbackOrder(bool concurrently)
                 await host.StartAsync();
                 await host.StopAsync();
 
-                Assert.Equal(1, impl._startingOrder);
-                Assert.Equal(2, impl._startOrder);
-                Assert.Equal(3, impl._startedOrder);
-                Assert.Equal(4, impl._applicationStartedOrder);
-                Assert.Equal(5, impl._stoppingOrder);
-                Assert.Equal(6, impl._applicationStoppingOrder);
-                Assert.Equal(7, impl._stopOrder);
-                Assert.Equal(8, impl._stoppedOrder);
-                Assert.Equal(9, impl._applicationStoppedOrder);
+                Assert.Equal(1, impl._hostWaitForStartAsyncOrder);
+                Assert.Equal(2, impl._startingOrder);
+                Assert.Equal(3, impl._startOrder);
+                Assert.Equal(4, impl._startedOrder);
+                Assert.Equal(5, impl._applicationStartedOrder);
+                Assert.Equal(6, impl._stoppingOrder);
+                Assert.Equal(7, impl._applicationStoppingOrder);
+                Assert.Equal(8, impl._stopOrder);
+                Assert.Equal(9, impl._stoppedOrder);
+                Assert.Equal(10, impl._applicationStoppedOrder);
+                Assert.Equal(11, impl._hostStoppedOrder);
             }
         }
 
-        private class CallbackOrder_Impl : IHostedLifecycleService
+        private class CallbackOrder_Impl : IHostedLifecycleService, IHostLifetime
         {
+            public int _hostWaitForStartAsyncOrder;
             public int _startingOrder;
             public int _startOrder;
             public int _startedOrder;
@@ -94,6 +98,7 @@ private class CallbackOrder_Impl : IHostedLifecycleService
             public int _stopOrder;
             public int _stoppedOrder;
             public int _applicationStoppedOrder;
+            public int _hostStoppedOrder;
 
             private int _callCount;
 
@@ -117,6 +122,12 @@ public CallbackOrder_Impl(IServiceProvider provider)
                 });
             }
 
+            Task IHostLifetime.WaitForStartAsync(CancellationToken cancellationToken)
+            {
+                _hostWaitForStartAsyncOrder = ++_callCount;
+                return Task.CompletedTask;
+            }
+
             public Task StartingAsync(CancellationToken cancellationToken)
             {
                 _startingOrder = ++_callCount;
@@ -151,6 +162,12 @@ public Task StoppedAsync(CancellationToken cancellationToken)
                 _stoppedOrder = ++_callCount;
                 return Task.CompletedTask;
             }
+
+            Task IHostLifetime.StopAsync(System.Threading.CancellationToken cancellationToken)
+            {
+                _hostStoppedOrder = ++_callCount;
+                return Task.CompletedTask;
+            }
         }
 
         private class ExceptionImpl : IHostedLifecycleService

From daf2c474c5998ce8d3f9fe99b8d195d58074b0ff Mon Sep 17 00:00:00 2001
From: Steve Harter <steveharter@users.noreply.github.com>
Date: Thu, 29 Jun 2023 16:12:06 -0500
Subject: [PATCH 4/5] Share code across callbacks

---
 .../src/Internal/Host.cs                      | 497 +++---------------
 .../tests/UnitTests/Internal/HostTests.cs     |   4 +-
 2 files changed, 86 insertions(+), 415 deletions(-)

diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
index 932d8d757df8c2..6c3869eef53570 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
@@ -91,68 +91,31 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
                 List<Exception> exceptions = new();
                 _hostedServices = Services.GetRequiredService<IEnumerable<IHostedService>>();
                 _hostedLifecycleServices = GetHostLifecycles(_hostedServices);
+                bool concurrent = _options.ServicesStartConcurrently;
+                bool abortOnFirstException = !concurrent;
 
-                // Run IHostedService.Start either concurrently or serially.
-                if (_options.ServicesStartConcurrently)
+                if (_hostedLifecycleServices is not null)
                 {
-                    if (_hostedLifecycleServices is not null)
-                    {
-                        await CallLifeCycle_Starting(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false);
-                    }
-
-                    await CallLifeCycle_Start(_hostedServices, token, exceptions).ConfigureAwait(false);
-
-                    if (_hostedLifecycleServices is not null)
-                    {
-                        await CallLifeCycle_Started(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false);
-                    }
+                    // Call StartingAsync().
+                    await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, (service, token) => service.StartingAsync(token)).ConfigureAwait(false);
                 }
-                else
-                {
-                    if (_hostedLifecycleServices is not null)
-                    {
-                        foreach (IHostedLifecycleService hostedService in _hostedLifecycleServices)
-                        {
-                            try
-                            {
-                                await hostedService.StartingAsync(token).ConfigureAwait(false);
-                            }
-                            catch (Exception ex)
-                            {
-                                exceptions.Add(ex);
-                                break;
-                            }
-                        }
-                    }
 
-                    foreach (IHostedService hostedService in _hostedServices)
+                // Call StartAsync().
+                await ForeachService(_hostedServices, token, concurrent, abortOnFirstException, exceptions,
+                    async (service, token) =>
                     {
-                        try
-                        {
-                            await StartAndTryToExecuteAsync(hostedService, token).ConfigureAwait(false);
-                        }
-                        catch (Exception ex)
-                        {
-                            exceptions.Add(ex);
-                            break;
-                        }
-                    }
+                        await service.StartAsync(token).ConfigureAwait(false);
 
-                    if (_hostedLifecycleServices is not null)
-                    {
-                        foreach (IHostedLifecycleService hostedService in _hostedLifecycleServices)
+                        if (service is BackgroundService backgroundService)
                         {
-                            try
-                            {
-                                await hostedService.StartedAsync(token).ConfigureAwait(false);
-                            }
-                            catch (Exception ex)
-                            {
-                                exceptions.Add(ex);
-                                break;
-                            }
+                            _ = TryExecuteBackgroundServiceAsync(backgroundService);
                         }
-                    }
+                    }).ConfigureAwait(false);
+
+                if (_hostedLifecycleServices is not null)
+                {
+                    // Call StartedAsync().
+                    await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, (service, token) => service.StartedAsync(token)).ConfigureAwait(false);
                 }
 
                 if (exceptions.Count > 0)
@@ -172,7 +135,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
                     }
                 }
 
-                // Fire IHostApplicationLifetime.Started
+                // Call IHostApplicationLifetime.Started
                 // This catches all exceptions and does not re-throw.
                 _applicationLifetime.NotifyStarted();
             }
@@ -180,16 +143,6 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
             _logger.Started();
         }
 
-        private async Task StartAndTryToExecuteAsync(IHostedService service, CancellationToken token)
-        {
-            await service.StartAsync(token).ConfigureAwait(false);
-
-            if (service is BackgroundService backgroundService)
-            {
-                _ = TryExecuteBackgroundServiceAsync(backgroundService);
-            }
-        }
-
         private async Task TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
         {
             // backgroundService.ExecuteTask may not be set (e.g. if the derived class doesn't call base.StartAsync)
@@ -253,11 +206,13 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
             using (linkedCts)
             {
                 CancellationToken token = linkedCts.Token;
-                List<Exception> exceptions = new();
 
-                if (_hostedServices is null)
+                List<Exception> exceptions = new();
+                if (_hostedServices is null) // Started?
                 {
-                    // Trigger IHostApplicationLifetime.ApplicationStopping even if StartAsync wasn't called.
+
+                    // Call IHostApplicationLifetime.ApplicationStopping.
+                    // This catches all exceptions and does not re-throw.
                     _applicationLifetime.StopApplication();
                 }
                 else
@@ -265,76 +220,29 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
                     // Ensure hosted services are stopped in LIFO order
                     IEnumerable<IHostedService> reversedServices = _hostedServices.Reverse();
                     IEnumerable<IHostedLifecycleService>? reversedLifetimeServices = _hostedLifecycleServices?.Reverse();
+                    bool concurrent = _options.ServicesStopConcurrently;
 
-                    if (_options.ServicesStopConcurrently)
+                    // Call StoppingAsync().
+                    if (reversedLifetimeServices is not null)
                     {
-                        if (reversedLifetimeServices is not null)
-                        {
-                            await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false);
-                        }
-
-                        // Trigger IHostApplicationLifetime.ApplicationStopping.
-                        // This catches all exceptions and does not re-throw.
-                        _applicationLifetime.StopApplication();
-
-                        await CallLifeCycle_Stop(reversedServices, token, exceptions).ConfigureAwait(false);
-
-                        if (reversedLifetimeServices is not null)
-                        {
-                            await CallLifeCycle_Stopped(reversedLifetimeServices, token, exceptions).ConfigureAwait(false);
-                        }
+                        await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StoppingAsync(token)).ConfigureAwait(false);
                     }
-                    else
-                    {
-                        if (reversedLifetimeServices is not null)
-                        {
-                            foreach (IHostedLifecycleService hostedService in reversedLifetimeServices)
-                            {
-                                try
-                                {
-                                    await hostedService.StoppingAsync(token).ConfigureAwait(false);
-                                }
-                                catch (Exception ex)
-                                {
-                                    exceptions.Add(ex);
-                                }
-                            }
-                        }
 
-                        // Trigger IHostApplicationLifetime.ApplicationStopping.
-                        // This catches all exceptions and does not re-throw.
-                        _applicationLifetime.StopApplication();
+                    // Call IHostApplicationLifetime.ApplicationStopping.
+                    // This catches all exceptions and does not re-throw.
+                    _applicationLifetime.StopApplication();
 
-                        foreach (IHostedService hostedService in reversedServices)
-                        {
-                            try
-                            {
-                                await hostedService.StopAsync(token).ConfigureAwait(false);
-                            }
-                            catch (Exception ex)
-                            {
-                                exceptions.Add(ex);
-                            }
-                        }
+                    // Call StopAsync().
+                    await ForeachService(reversedServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StopAsync(token)).ConfigureAwait(false);
 
-                        if (reversedLifetimeServices is not null)
-                        {
-                            foreach (IHostedLifecycleService hostedService in reversedLifetimeServices)
-                            {
-                                try
-                                {
-                                    await hostedService.StoppedAsync(token).ConfigureAwait(false);
-                                }
-                                catch (Exception ex)
-                                {
-                                    exceptions.Add(ex);
-                                }
-                            }
-                        }
+                    if (reversedLifetimeServices is not null)
+                    {
+                        // Call StoppedAsync().
+                        await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StoppedAsync(token)).ConfigureAwait(false);
                     }
                 }
 
-                // Fire IHostApplicationLifetime.Stopped
+                // Call IHostApplicationLifetime.Stopped
                 // This catches all exceptions and does not re-throw.
                 _applicationLifetime.NotifyStopped();
 
@@ -369,315 +277,78 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
             _logger.Stopped();
         }
 
-        /// <summary>
-        /// Call <see cref="IHostedLifecycleService.StartingAsync(CancellationToken)"/>.
-        /// 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 run concurrently.
-        /// </summary>
-        private static async Task CallLifeCycle_Starting(IEnumerable<IHostedLifecycleService> services, CancellationToken token, List<Exception> exceptions)
+        private static async Task ForeachService<T>(
+            IEnumerable<T> services,
+            CancellationToken token,
+            bool concurrent,
+            bool abortOnFirstException,
+            List<Exception> exceptions,
+            Func<T, CancellationToken, Task> operation)
         {
-            List<Task>? tasks = null;
-
-            foreach (IHostedLifecycleService service in services)
+            if (concurrent)
             {
-                Task task;
-                try
-                {
-                    task = service.StartingAsync(token);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.Add(ex); // Log exception from sync method.
-                    continue;
-                }
+                // 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 run concurrently.
+                List<Task>? tasks = null;
 
-                if (task.IsCompleted)
+                foreach (T service in services)
                 {
-                    if (task.Exception is not null)
+                    Task task;
+                    try
                     {
-                        exceptions.Add(task.Exception); // Log exception from async method.
+                        task = operation(service, token);
                     }
-                }
-                else
-                {
-                    tasks ??= new();
-                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
-                }
-            }
-
-            if (tasks is not null)
-            {
-                Task groupedTasks = Task.WhenAll(tasks);
-
-                try
-                {
-                    await groupedTasks.ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
-                }
-            }
-        }
-
-        /// <summary>
-        /// Call <see cref="IHostedService.StartAsync(CancellationToken)"/>.
-        /// 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 run concurrently.
-        /// </summary>
-        private static async Task CallLifeCycle_Start(IEnumerable<IHostedService> services, CancellationToken token, List<Exception> exceptions)
-        {
-            List<Task>? tasks = null;
-
-            foreach (IHostedService service in services)
-            {
-                Task task;
-                try
-                {
-                    task = service.StartAsync(token);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.Add(ex); // Log exception from sync method.
-                    continue;
-                }
-
-                if (task.IsCompleted)
-                {
-                    if (task.Exception is not null)
+                    catch (Exception ex)
                     {
-                        exceptions.Add(task.Exception); // Log exception from async method.
+                        exceptions.Add(ex); // Log exception from sync method.
+                        continue;
                     }
-                }
-                else
-                {
-                    tasks ??= new();
-                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
-                }
-            }
-
-            if (tasks is not null)
-            {
-                Task groupedTasks = Task.WhenAll(tasks);
-
-                try
-                {
-                    await groupedTasks.ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
-                }
-            }
-        }
-
-        /// <summary>
-        /// Call <see cref="IHostedLifecycleService.StartedAsync(CancellationToken)"/>.
-        /// 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 run concurrently.
-        /// </summary>
-        private static async Task CallLifeCycle_Started(IEnumerable<IHostedLifecycleService> services, CancellationToken token, List<Exception> exceptions)
-        {
-            List<Task>? tasks = null;
 
-            foreach (IHostedLifecycleService service in services)
-            {
-                Task task;
-                try
-                {
-                    task = service.StartedAsync(token);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.Add(ex); // Log exception from sync method.
-                    continue;
-                }
-
-                if (task.IsCompleted)
-                {
-                    if (task.Exception is not null)
+                    if (task.IsCompleted)
                     {
-                        exceptions.Add(task.Exception); // Log exception from async method.
+                        if (task.Exception is not null)
+                        {
+                            exceptions.Add(task.Exception); // Log exception from async method.
+                        }
                     }
-                }
-                else
-                {
-                    tasks ??= new();
-                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
-                }
-
-            }
-
-            if (tasks is not null)
-            {
-                Task groupedTasks = Task.WhenAll(tasks);
-
-                try
-                {
-                    await groupedTasks.ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
-                }
-            }
-        }
-
-        /// <summary>
-        /// Call <see cref="IHostedLifecycleService.StoppingAsync(CancellationToken)"/>.
-        /// 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 run concurrently.
-        /// </summary>
-        private static async Task CallLifeCycle_Stopping(IEnumerable<IHostedLifecycleService> services, CancellationToken token, List<Exception> exceptions)
-        {
-            List<Task>? tasks = null;
-
-            foreach (IHostedLifecycleService service in services)
-            {
-                Task task;
-                try
-                {
-                    task = service.StoppingAsync(token);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.Add(ex); // Log exception from sync method.
-                    continue;
-                }
-
-                if (task.IsCompleted)
-                {
-                    if (task.Exception is not null)
+                    else
                     {
-                        exceptions.Add(task.Exception); // Log exception from async method.
+                        tasks ??= new();
+                        tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
                     }
                 }
-                else
-                {
-                    tasks ??= new();
-                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
-                }
-            }
-
-            if (tasks is not null)
-            {
-                Task groupedTasks = Task.WhenAll(tasks);
 
-                try
-                {
-                    await groupedTasks.ConfigureAwait(false);
-                }
-                catch (Exception ex)
+                if (tasks is not null)
                 {
-                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
-                }
-            }
-        }
-
-        /// <summary>
-        /// Call <see cref="IHostedService.StopAsync(CancellationToken)"/>.
-        /// 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 run concurrently.
-        /// </summary>
-        private static async Task CallLifeCycle_Stop(IEnumerable<IHostedService> services, CancellationToken token, List<Exception> exceptions)
-        {
-            List<Task>? tasks = null;
+                    Task groupedTasks = Task.WhenAll(tasks);
 
-            foreach (IHostedService service in services)
-            {
-                Task task;
-                try
-                {
-                    task = service.StopAsync(token);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.Add(ex); // Log exception from sync method.
-                    continue;
-                }
-
-                if (task.IsCompleted)
-                {
-                    if (task.Exception is not null)
+                    try
                     {
-                        exceptions.Add(task.Exception); // Log exception from async method.
+                        await groupedTasks.ConfigureAwait(false);
                     }
-                }
-                else
-                {
-                    tasks ??= new();
-                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
-                }
-            }
-
-            if (tasks is not null)
-            {
-                Task groupedTasks = Task.WhenAll(tasks);
-
-                try
-                {
-                    await groupedTasks.ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
-                }
-            }
-        }
-
-        /// <summary>
-        /// Call <see cref="IHostedLifecycleService.StoppedAsync(CancellationToken)"/>.
-        /// 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 run concurrently.
-        /// </summary>
-        private static async Task CallLifeCycle_Stopped(IEnumerable<IHostedLifecycleService> services, CancellationToken token, List<Exception> exceptions)
-        {
-            List<Task>? tasks = null;
-
-            foreach (IHostedLifecycleService service in services)
-            {
-                Task task;
-                try
-                {
-                    task = service.StoppedAsync(token);
-                }
-                catch (Exception ex)
-                {
-                    exceptions.Add(ex); // Log exception from sync method.
-                    continue;
-                }
-
-                if (task.IsCompleted)
-                {
-                    if (task.Exception is not null)
+                    catch (Exception ex)
                     {
-                        exceptions.Add(task.Exception); // Log exception from async method.
+                        exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
                     }
                 }
-                else
-                {
-                    tasks ??= new();
-                    tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
-                }
             }
-
-            if (tasks is not null)
+            else
             {
-                Task groupedTasks = Task.WhenAll(tasks);
-
-                try
-                {
-                    await groupedTasks.ConfigureAwait(false);
-                }
-                catch (Exception ex)
+                foreach (T service in services)
                 {
-                    exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+                    try
+                    {
+                        await operation(service, token).ConfigureAwait(false);
+                    }
+                    catch (Exception ex)
+                    {
+                        exceptions.Add(ex);
+                        if (abortOnFirstException)
+                        {
+                            return;
+                        }
+                    }
                 }
             }
         }
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
index 23cf84e91621d3..4f9aa27b2f4395 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
@@ -237,7 +237,7 @@ public async Task AppCrashesOnStartWhenFirstHostedServiceThrows(int eventCount,
                 })
                 .Build())
             {
-                if (concurrentStartup && eventCount > 1)
+                if (concurrentStartup)
                 {
                     await Assert.ThrowsAsync<AggregateException>(() => host.StartAsync());
                 }
@@ -1159,7 +1159,7 @@ public async Task HostDoesNotNotifyIHostApplicationLifetimeCallbacksIfIHostedSer
                 var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted);
                 var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
 
-                if (concurrentStartup && eventCount > 1)
+                if (concurrentStartup)
                 {
                     await Assert.ThrowsAsync<AggregateException>(() => host.StartAsync());
                 }

From cdf42fcd293023e027fcc361b0545ef1b6d0f7bc Mon Sep 17 00:00:00 2001
From: Steve Harter <steveharter@users.noreply.github.com>
Date: Fri, 30 Jun 2023 10:28:01 -0500
Subject: [PATCH 5/5] Revert prior test change; fix aggregate exception

---
 .../src/Internal/Host.cs                      | 19 ++++++++++++-------
 .../tests/UnitTests/Internal/HostTests.cs     |  4 ++--
 .../tests/UnitTests/LifecycleTests.Start.cs   |  5 -----
 .../tests/UnitTests/LifecycleTests.Stop.cs    |  4 ----
 4 files changed, 14 insertions(+), 18 deletions(-)

diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
index 6c3869eef53570..c64f899fc4dfaf 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
@@ -97,7 +97,8 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
                 if (_hostedLifecycleServices is not null)
                 {
                     // Call StartingAsync().
-                    await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, (service, token) => service.StartingAsync(token)).ConfigureAwait(false);
+                    await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions,
+                        (service, token) => service.StartingAsync(token)).ConfigureAwait(false);
                 }
 
                 // Call StartAsync().
@@ -115,7 +116,8 @@ await ForeachService(_hostedServices, token, concurrent, abortOnFirstException,
                 if (_hostedLifecycleServices is not null)
                 {
                     // Call StartedAsync().
-                    await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, (service, token) => service.StartedAsync(token)).ConfigureAwait(false);
+                    await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions,
+                        (service, token) => service.StartedAsync(token)).ConfigureAwait(false);
                 }
 
                 if (exceptions.Count > 0)
@@ -225,7 +227,8 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
                     // Call StoppingAsync().
                     if (reversedLifetimeServices is not null)
                     {
-                        await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StoppingAsync(token)).ConfigureAwait(false);
+                        await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions,
+                            (service, token) => service.StoppingAsync(token)).ConfigureAwait(false);
                     }
 
                     // Call IHostApplicationLifetime.ApplicationStopping.
@@ -233,12 +236,14 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
                     _applicationLifetime.StopApplication();
 
                     // Call StopAsync().
-                    await ForeachService(reversedServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StopAsync(token)).ConfigureAwait(false);
+                    await ForeachService(reversedServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) =>
+                        service.StopAsync(token)).ConfigureAwait(false);
 
                     if (reversedLifetimeServices is not null)
                     {
                         // Call StoppedAsync().
-                        await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StoppedAsync(token)).ConfigureAwait(false);
+                        await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) =>
+                            service.StoppedAsync(token)).ConfigureAwait(false);
                     }
                 }
 
@@ -309,13 +314,13 @@ private static async Task ForeachService<T>(
                     {
                         if (task.Exception is not null)
                         {
-                            exceptions.Add(task.Exception); // Log exception from async method.
+                            exceptions.AddRange(task.Exception.InnerExceptions); // Log exception from async method.
                         }
                     }
                     else
                     {
                         tasks ??= new();
-                        tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token));
+                        tasks.Add(Task.Run(() => task, token));
                     }
                 }
 
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
index 4f9aa27b2f4395..23cf84e91621d3 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
@@ -237,7 +237,7 @@ public async Task AppCrashesOnStartWhenFirstHostedServiceThrows(int eventCount,
                 })
                 .Build())
             {
-                if (concurrentStartup)
+                if (concurrentStartup && eventCount > 1)
                 {
                     await Assert.ThrowsAsync<AggregateException>(() => host.StartAsync());
                 }
@@ -1159,7 +1159,7 @@ public async Task HostDoesNotNotifyIHostApplicationLifetimeCallbacksIfIHostedSer
                 var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted);
                 var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
 
-                if (concurrentStartup)
+                if (concurrentStartup && eventCount > 1)
                 {
                     await Assert.ThrowsAsync<AggregateException>(() => host.StartAsync());
                 }
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs
index 1de8c2b9c494fd..31692fe563ef22 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs
@@ -117,7 +117,6 @@ public async Task StartConcurrently()
                 // Both run serially until the await.
                 Task start = host.StartAsync();
                 Verify(1, 1, 0, 0, 0, 0);
-                await Task.Delay(s_superShortDelay);
                 Verify(1, 1, 0, 0, 0, 0);
 
                 // Resume and check that both are not finished.
@@ -329,10 +328,6 @@ public async Task StartPhasesException(bool throwAfterAsyncCall)
                 Assert.True(impl.StartCalled);
                 Assert.True(impl.StartedCalled);
 
-                Assert.Contains("(ThrowOnStarting)", ex.Message);
-                Assert.Contains("(ThrowOnStart)", ex.Message);
-                Assert.Contains("(ThrowOnStarted)", ex.Message);
-
                 Assert.Equal(3, ex.InnerExceptions.Count);
                 Assert.Contains("(ThrowOnStarting)", ex.InnerExceptions[0].Message);
                 Assert.Contains("(ThrowOnStart)", ex.InnerExceptions[1].Message);
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs
index 03b684df4087bf..2b413d29be3b22 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs
@@ -340,10 +340,6 @@ public async Task StopPhasesException(bool throwAfterAsyncCall)
                 Assert.True(impl.StopCalled);
                 Assert.True(impl.StoppedCalled);
 
-                Assert.Contains("(ThrowOnStopping)", ex.Message);
-                Assert.Contains("(ThrowOnStop)", ex.Message);
-                Assert.Contains("(ThrowOnStopped)", ex.Message);
-
                 Assert.Equal(3, ex.InnerExceptions.Count);
                 Assert.Contains("(ThrowOnStopping)", ex.InnerExceptions[0].Message);
                 Assert.Contains("(ThrowOnStop)", ex.InnerExceptions[1].Message);
