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 085bc47a266b07..d8820aa7133668 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs
@@ -105,6 +105,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..c6c29dc10cfb2f 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. Defaults to false.
+ ///
+ public bool ServicesStartConcurrently { get; set; }
+
+ ///
+ /// Determines if the will stop registered instances of concurrently or sequentially. Defaults to false.
+ ///
+ public bool ServicesStopConcurrently { get; set; }
+
///
/// The behavior the will follow when any of
/// its instances throw an unhandled exception.
@@ -35,6 +45,20 @@ internal void Initialize(IConfiguration configuration)
{
ShutdownTimeout = TimeSpan.FromSeconds(seconds);
}
+
+ var servicesStartConcurrently = configuration["servicesStartConcurrently"];
+ if (!string.IsNullOrEmpty(servicesStartConcurrently)
+ && bool.TryParse(servicesStartConcurrently, out bool startBehavior))
+ {
+ ServicesStartConcurrently = startBehavior;
+ }
+
+ var servicesStopConcurrently = configuration["servicesStopConcurrently"];
+ if (!string.IsNullOrEmpty(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 33dcc552b8339f..fcc55b2f5d43ec 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,65 @@ 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 ?? new[] { ex }.AsEnumerable());
+ }
+ }
+ 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
+ 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;
}
}
@@ -128,15 +180,34 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
var exceptions = new List();
if (_hostedServices != null) // Started?
{
- foreach (IHostedService hostedService in _hostedServices.Reverse())
+ // Ensure hosted services are stopped in LIFO 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 ?? new[] { ex }.AsEnumerable());
+ }
+ }
+ else
+ {
+ foreach (IHostedService hostedService in hostedServices)
+ {
+ try
+ {
+ await hostedService.StopAsync(token).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ }
}
}
}
@@ -155,9 +226,19 @@ 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
+ 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/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/DelegateHostedService.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DelegateHostedService.cs
new file mode 100644
index 00000000000000..6f6b319138f90b
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DelegateHostedService.cs
@@ -0,0 +1,40 @@
+// 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;
+
+namespace Microsoft.Extensions.Hosting.Unit.Tests;
+
+internal class DelegateHostedService : IHostedService, IDisposable
+{
+ private readonly Action _started;
+ private readonly Action _stopping;
+ private readonly Action _disposing;
+
+ public DelegateHostedService(Action started, Action stopping, Action disposing)
+ {
+ _started = started;
+ _stopping = stopping;
+ _disposing = disposing;
+ }
+
+ public Task StartAsync(CancellationToken token)
+ {
+ StartDate = DateTimeOffset.Now;
+ _started();
+ return Task.CompletedTask;
+ }
+ public Task StopAsync(CancellationToken token)
+ {
+ StopDate = DateTimeOffset.Now;
+ _stopping();
+ return Task.CompletedTask;
+ }
+
+ public void Dispose() => _disposing();
+
+ public DateTimeOffset StartDate { get; private set; }
+ public DateTimeOffset StopDate { get; private set; }
+}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
index eef367f8355891..4b678b8912b77e 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
@@ -9,13 +9,13 @@
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting.Unit.Tests;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Xunit;
@@ -36,6 +36,77 @@ public async Task StopAsyncWithCancellation()
await host.StopAsync(cts.Token);
}
+ public static IEnumerable