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 7910670857169a..7a6eb03df41ec7 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs
@@ -114,6 +114,8 @@ public partial class HostOptions
{
public HostOptions() { }
public Microsoft.Extensions.Hosting.BackgroundServiceExceptionBehavior BackgroundServiceExceptionBehavior { get { throw null; } set { } }
+ public bool ServicesStartConcurrently { get { throw null; } set { } }
+ public bool ServicesStopConcurrently { get { throw null; } set { } }
public System.TimeSpan ShutdownTimeout { get { throw null; } set { } }
}
}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs
index ac7cc3f4863b3f..e6da525fc095e1 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs
@@ -17,6 +17,16 @@ public class HostOptions
///
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30);
+ ///
+ /// Determines if the will start registered instances of concurrently or sequentially
+ ///
+ public bool ServicesStartConcurrently { get; set; }
+
+ ///
+ /// Determines if the will stop registered instances of concurrently or sequentially
+ ///
+ public bool ServicesStopConcurrently { get; set; }
+
///
/// The behavior the will follow when any of
/// its instances throw an unhandled exception.
@@ -30,11 +40,25 @@ public class HostOptions
internal void Initialize(IConfiguration configuration)
{
var timeoutSeconds = configuration["shutdownTimeoutSeconds"];
- if (!string.IsNullOrEmpty(timeoutSeconds)
+ if (!string.IsNullOrWhiteSpace(timeoutSeconds)
&& int.TryParse(timeoutSeconds, NumberStyles.None, CultureInfo.InvariantCulture, out var seconds))
{
ShutdownTimeout = TimeSpan.FromSeconds(seconds);
}
+
+ var servicesStartConcurrently = configuration["servicesStartConcurrently"];
+ if (!string.IsNullOrWhiteSpace(servicesStartConcurrently)
+ && bool.TryParse(servicesStartConcurrently, out bool startBehavior))
+ {
+ ServicesStartConcurrently = startBehavior;
+ }
+
+ var servicesStopConcurrently = configuration["servicesStopConcurrently"];
+ if (!string.IsNullOrWhiteSpace(servicesStopConcurrently)
+ && bool.TryParse(servicesStopConcurrently, out bool stopBehavior))
+ {
+ ServicesStopConcurrently = stopBehavior;
+ }
}
}
}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
index f0339b80020968..6b2daebcf8d850 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
@@ -65,14 +66,64 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
combinedCancellationToken.ThrowIfCancellationRequested();
_hostedServices = Services.GetRequiredService>();
- foreach (IHostedService hostedService in _hostedServices)
+ List exceptions = new List();
+
+ if (_options.ServicesStartConcurrently)
{
- // Fire IHostedService.Start
- await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
+ Task tasks = Task.WhenAll(_hostedServices.Select(async service =>
+ {
+ await service.StartAsync(combinedCancellationToken).ConfigureAwait(false);
+
+ if (service is BackgroundService backgroundService)
+ {
+ _ = TryExecuteBackgroundServiceAsync(backgroundService);
+ }
+ }));
- if (hostedService is BackgroundService backgroundService)
+ try
{
- _ = TryExecuteBackgroundServiceAsync(backgroundService);
+ await tasks.ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ exceptions.AddRange(tasks.Exception?.InnerExceptions?.ToArray() ?? new[] { ex });
+ }
+ }
+ else
+ {
+ foreach (IHostedService hostedService in _hostedServices)
+ {
+ try
+ {
+ // Fire IHostedService.Start
+ await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
+
+ if (hostedService is BackgroundService backgroundService)
+ {
+ _ = TryExecuteBackgroundServiceAsync(backgroundService);
+ }
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ break;
+ }
+ }
+ }
+
+ if (exceptions.Count > 0)
+ {
+ if (exceptions.Count == 1)
+ {
+ // Rethrow if it's a single error
+ _logger.HostedServiceStartupFaulted(exceptions[0]);
+ ExceptionDispatchInfo.Capture(exceptions[0]).Throw();
+ }
+ else
+ {
+ var ex = new AggregateException("One or more hosted services failed to start.", exceptions);
+ _logger.HostedServiceStartupFaulted(ex);
+ throw ex;
}
}
@@ -125,18 +176,37 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
// Trigger IHostApplicationLifetime.ApplicationStopping
_applicationLifetime.StopApplication();
- IList exceptions = new List();
- if (_hostedServices != null) // Started?
+ List exceptions = new List();
+ if (_hostedServices?.Any() == true) // Started?
{
- foreach (IHostedService hostedService in _hostedServices.Reverse())
+ // Ensure hosted services are stopped in FILO order
+ IEnumerable hostedServices = _hostedServices.Reverse();
+
+ if (_options.ServicesStopConcurrently)
{
+ Task tasks = Task.WhenAll(hostedServices.Select(async service => await service.StopAsync(token).ConfigureAwait(false)));
+
try
{
- await hostedService.StopAsync(token).ConfigureAwait(false);
+ await tasks.ConfigureAwait(false);
}
catch (Exception ex)
{
- exceptions.Add(ex);
+ exceptions.AddRange(tasks.Exception?.InnerExceptions?.ToArray() ?? new[] { ex });
+ }
+ }
+ else
+ {
+ foreach (IHostedService hostedService in hostedServices)
+ {
+ try
+ {
+ await hostedService.StopAsync(token).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ }
}
}
}
@@ -155,9 +225,18 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
if (exceptions.Count > 0)
{
- 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
+ _logger.HostedServiceStartupFaulted(exceptions[0]);
+ ExceptionDispatchInfo.Capture(exceptions[0]).Throw();
+ }
+ else
+ {
+ var ex = new AggregateException("One or more hosted services failed to stop.", exceptions);
+ _logger.HostedServiceStartupFaulted(ex);
+ throw ex;
+ }
}
}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs
index fc4daf79a27da6..71354c63819cac 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs
@@ -101,5 +101,16 @@ public static void BackgroundServiceStoppingHost(this ILogger logger, Exception?
message: SR.BackgroundServiceExceptionStoppedHost);
}
}
+
+ public static void HostedServiceStartupFaulted(this ILogger logger, Exception? ex)
+ {
+ if (logger.IsEnabled(LogLevel.Error))
+ {
+ logger.LogError(
+ eventId: LoggerEventIds.HostedServiceStartupFaulted,
+ exception: ex,
+ message: "Hosting failed to start");
+ }
+ }
}
}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs
index 9271953313a05c..64666cd09a0097 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs
@@ -17,5 +17,6 @@ internal static class LoggerEventIds
public static readonly EventId ApplicationStoppedException = new EventId(8, nameof(ApplicationStoppedException));
public static readonly EventId BackgroundServiceFaulted = new EventId(9, nameof(BackgroundServiceFaulted));
public static readonly EventId BackgroundServiceStoppingHost = new EventId(10, nameof(BackgroundServiceStoppingHost));
+ public static readonly EventId HostedServiceStartupFaulted = new EventId(11, nameof(HostedServiceStartupFaulted));
}
}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
index c4cb6ce4558108..c5b1da829d975c 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
@@ -16,8 +16,10 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Moq;
using Xunit;
namespace Microsoft.Extensions.Hosting.Tests
@@ -36,6 +38,114 @@ public async Task StopAsyncWithCancellation()
await host.StopAsync(cts.Token);
}
+ public static IEnumerable