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 1f92e812f2fc8..a2b2b83ae5d56 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs
@@ -13,6 +13,11 @@ public static partial class OptionsBuilderExtensions
}
namespace Microsoft.Extensions.Hosting
{
+ public enum BackgroundServiceExceptionBehavior
+ {
+ StopHost,
+ Ignore
+ }
public partial class ConsoleLifetimeOptions
{
public ConsoleLifetimeOptions() { }
@@ -56,6 +61,7 @@ public partial class HostOptions
{
public HostOptions() { }
public System.TimeSpan ShutdownTimeout { get { throw null; } set { } }
+ public Microsoft.Extensions.Hosting.BackgroundServiceExceptionBehavior BackgroundServiceExceptionBehavior { get { throw null; } set { } }
}
}
namespace Microsoft.Extensions.Hosting.Internal
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs b/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs
new file mode 100644
index 0000000000000..7b3b66fc1bf3f
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Hosting
+{
+ ///
+ /// Specifies a behavior that the will honor if an
+ /// unhandled exception occurs in one of its instances.
+ ///
+ public enum BackgroundServiceExceptionBehavior
+ {
+ ///
+ /// Stops the instance.
+ ///
+ ///
+ /// If a throws an exception, the instance stops, and the process continues.
+ ///
+ StopHost = 0,
+
+ ///
+ /// Ignore exceptions thrown in .
+ ///
+ ///
+ /// If a throws an exception, the will log the error, but otherwise ignore it.
+ /// The is not restarted.
+ ///
+ Ignore = 1
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs
index 3b825718642d0..208d945678db7 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(5);
+ ///
+ /// The behavior the will follow when any of
+ /// its instances throw an unhandled exception.
+ ///
+ ///
+ /// Defaults to .
+ ///
+ public BackgroundServiceExceptionBehavior BackgroundServiceExceptionBehavior { get; set; } =
+ BackgroundServiceExceptionBehavior.StopHost;
+
internal void Initialize(IConfiguration configuration)
{
var timeoutSeconds = configuration["shutdownTimeoutSeconds"];
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
index ca78d42a05bc6..bcaed337120e2 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
@@ -66,7 +66,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
if (hostedService is BackgroundService backgroundService)
{
- _ = HandleBackgroundException(backgroundService);
+ _ = TryExecuteBackgroundServiceAsync(backgroundService);
}
}
@@ -76,7 +76,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
_logger.Started();
}
- private async Task HandleBackgroundException(BackgroundService backgroundService)
+ private async Task TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
{
try
{
@@ -85,6 +85,11 @@ private async Task HandleBackgroundException(BackgroundService backgroundService
catch (Exception ex)
{
_logger.BackgroundServiceFaulted(ex);
+ if (_options.BackgroundServiceExceptionBehavior == BackgroundServiceExceptionBehavior.StopHost)
+ {
+ _logger.BackgroundServiceStoppingHost(ex);
+ _applicationLifetime.StopApplication();
+ }
}
}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs
index a3c722f965336..32e560f1f74fd 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs
@@ -11,8 +11,7 @@ internal static class HostingLoggerExtensions
{
public static void ApplicationError(this ILogger logger, EventId eventId, string message, Exception exception)
{
- var reflectionTypeLoadException = exception as ReflectionTypeLoadException;
- if (reflectionTypeLoadException != null)
+ if (exception is ReflectionTypeLoadException reflectionTypeLoadException)
{
foreach (Exception ex in reflectionTypeLoadException.LoaderExceptions)
{
@@ -87,5 +86,16 @@ public static void BackgroundServiceFaulted(this ILogger logger, Exception ex)
message: "BackgroundService failed");
}
}
+
+ public static void BackgroundServiceStoppingHost(this ILogger logger, Exception ex)
+ {
+ if (logger.IsEnabled(LogLevel.Critical))
+ {
+ logger.LogCritical(
+ eventId: LoggerEventIds.BackgroundServiceStoppingHost,
+ exception: ex,
+ message: SR.BackgroundServiceExceptionStoppedHost);
+ }
+ }
}
}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs
index 666266c1d50d4..9271953313a05 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs
@@ -7,14 +7,15 @@ namespace Microsoft.Extensions.Hosting.Internal
{
internal static class LoggerEventIds
{
- public static readonly EventId Starting = new EventId(1, "Starting");
- public static readonly EventId Started = new EventId(2, "Started");
- public static readonly EventId Stopping = new EventId(3, "Stopping");
- public static readonly EventId Stopped = new EventId(4, "Stopped");
- public static readonly EventId StoppedWithException = new EventId(5, "StoppedWithException");
- public static readonly EventId ApplicationStartupException = new EventId(6, "ApplicationStartupException");
- public static readonly EventId ApplicationStoppingException = new EventId(7, "ApplicationStoppingException");
- public static readonly EventId ApplicationStoppedException = new EventId(8, "ApplicationStoppedException");
- public static readonly EventId BackgroundServiceFaulted = new EventId(9, "BackgroundServiceFaulted");
+ public static readonly EventId Starting = new EventId(1, nameof(Starting));
+ public static readonly EventId Started = new EventId(2, nameof(Started));
+ public static readonly EventId Stopping = new EventId(3, nameof(Stopping));
+ public static readonly EventId Stopped = new EventId(4, nameof(Stopped));
+ public static readonly EventId StoppedWithException = new EventId(5, nameof(StoppedWithException));
+ public static readonly EventId ApplicationStartupException = new EventId(6, nameof(ApplicationStartupException));
+ public static readonly EventId ApplicationStoppingException = new EventId(7, nameof(ApplicationStoppingException));
+ 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));
}
}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx
index c4de18cb0e1e1..90d8aad521a27 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx
+++ b/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx
@@ -1,13 +1,13 @@
-
@@ -117,6 +117,9 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost. A BackgroundService has thrown an unhandled exception, and the IHost instance is stopping. To avoid this behavior, configure this to Ignore; however the BackgroundService will not be restarted.
+
Build can only be called once.
@@ -129,4 +132,4 @@
The resolver returned a null IServiceProviderFactory
-
\ No newline at end of file
+
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs
index df2d42a574c4b..1e4721987d678 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs
@@ -572,6 +572,27 @@ public void HostBuilderConfigureDefaultsInterleavesMissingConfigValues()
Assert.Equal(expectedContentRootPath, env.ContentRootPath);
}
+ [Theory]
+ [InlineData(BackgroundServiceExceptionBehavior.Ignore)]
+ [InlineData(BackgroundServiceExceptionBehavior.StopHost)]
+ public void HostBuilderCanConfigureBackgroundServiceExceptionBehavior(
+ BackgroundServiceExceptionBehavior testBehavior)
+ {
+ using IHost host = new HostBuilder()
+ .ConfigureServices(
+ services =>
+ services.Configure(
+ options =>
+ options.BackgroundServiceExceptionBehavior = testBehavior))
+ .Build();
+
+ var options = host.Services.GetRequiredService>();
+
+ Assert.Equal(
+ testBehavior,
+ options.Value.BackgroundServiceExceptionBehavior);
+ }
+
private class FakeFileProvider : IFileProvider, IDisposable
{
public bool Disposed { 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 2136a23ef2bd9..aa5ad3d014a8f 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
@@ -145,7 +145,7 @@ public void CreateDefaultBuilder_EnablesValidateOnBuild()
[ActiveIssue("https://github.com/dotnet/runtime/issues/34580", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
public async Task CreateDefaultBuilder_ConfigJsonDoesNotReload()
{
- var reloadFlagConfig = new Dictionary() {{ "hostbuilder:reloadConfigOnChange", "false" }};
+ var reloadFlagConfig = new Dictionary() { { "hostbuilder:reloadConfigOnChange", "false" } };
var appSettingsPath = Path.Combine(Path.GetTempPath(), "appsettings.json");
string SaveRandomConfig()
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 f7ce5afc57ca7..4138e7df4c5c0 100644
--- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
+++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
@@ -657,6 +657,100 @@ public async Task WebHostStopAsyncUsesDefaultTimeoutIfNoTokenProvided()
}
}
+ [Fact]
+ public async Task HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBehaviorOfStopHost()
+ {
+ using IHost host = CreateBuilder()
+ .ConfigureServices(
+ services =>
+ {
+ services.AddHostedService(_ => new AsyncThrowingService(Task.CompletedTask));
+ services.Configure(
+ options =>
+ options.BackgroundServiceExceptionBehavior =
+ BackgroundServiceExceptionBehavior.StopHost);
+ })
+ .Build();
+
+ await Assert.ThrowsAsync(() => host.StartAsync());
+ }
+
+ [Fact]
+ public async Task HostStopsApplicationWithOneBackgroundServiceErrorAndOthersWithoutError()
+ {
+ var wasOtherServiceStarted = false;
+
+ TaskCompletionSource throwingTcs = new();
+ TaskCompletionSource otherTcs = new();
+
+ using IHost host = CreateBuilder()
+ .ConfigureServices(
+ services =>
+ {
+ services.AddHostedService(_ => new AsyncThrowingService(throwingTcs.Task));
+ services.AddHostedService(
+ _ => new TestBackgroundService(otherTcs.Task,
+ () =>
+ {
+ wasOtherServiceStarted = true;
+ throwingTcs.SetResult(true);
+ }));
+ services.Configure(
+ options =>
+ options.BackgroundServiceExceptionBehavior =
+ BackgroundServiceExceptionBehavior.StopHost);
+ })
+ .Build();
+
+ var lifetime = host.Services.GetRequiredService();
+
+ var wasStartedCalled = false;
+ lifetime.ApplicationStarted.Register(() => wasStartedCalled = true);
+
+ var wasStoppingCalled = false;
+ lifetime.ApplicationStopping.Register(() =>
+ {
+ otherTcs.SetResult(true);
+ wasStoppingCalled = true;
+ });
+
+ await host.StartAsync();
+
+ Assert.True(wasStartedCalled);
+ Assert.True(wasStoppingCalled);
+ Assert.True(wasOtherServiceStarted);
+ }
+
+ [Fact]
+ public void HostHandlesExceptionsThrownWithBackgroundServiceExceptionBehaviorOfIgnore()
+ {
+ var backgroundDelayTaskSource = new TaskCompletionSource();
+
+ using IHost host = CreateBuilder()
+ .ConfigureServices(
+ services =>
+ {
+ services.AddHostedService(
+ _ => new AsyncThrowingService(backgroundDelayTaskSource.Task));
+
+ services.PostConfigure(
+ options =>
+ options.BackgroundServiceExceptionBehavior =
+ BackgroundServiceExceptionBehavior.Ignore);
+ })
+ .Build();
+
+ var lifetime = host.Services.GetRequiredService();
+ var wasStoppingCalled = false;
+ lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true);
+
+ host.Start();
+
+ backgroundDelayTaskSource.SetResult(true);
+
+ Assert.False(wasStoppingCalled);
+ }
+
[Fact]
public void HostApplicationLifetimeEventsOrderedCorrectlyDuringShutdown()
{
@@ -1223,8 +1317,12 @@ public void ThrowExceptionForCustomImplementationOfIHostApplicationLifetime()
/// Tests when a BackgroundService throws an exception asynchronously
/// (after an await), the exception gets logged correctly.
///
- [Fact]
- public async Task BackgroundServiceAsyncExceptionGetsLogged()
+ [Theory]
+ [InlineData(BackgroundServiceExceptionBehavior.Ignore, "BackgroundService failed")]
+ [InlineData(BackgroundServiceExceptionBehavior.StopHost, "BackgroundService failed", "The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost")]
+ public async Task BackgroundServiceAsyncExceptionGetsLogged(
+ BackgroundServiceExceptionBehavior testBehavior,
+ params string[] expectedExceptionMessages)
{
using TestEventListener listener = new TestEventListener();
var backgroundDelayTaskSource = new TaskCompletionSource();
@@ -1236,6 +1334,9 @@ public async Task BackgroundServiceAsyncExceptionGetsLogged()
})
.ConfigureServices((hostContext, services) =>
{
+ services.Configure(
+ options =>
+ options.BackgroundServiceExceptionBehavior = testBehavior);
services.AddHostedService(sp => new AsyncThrowingService(backgroundDelayTaskSource.Task));
})
.Start();
@@ -1243,15 +1344,19 @@ public async Task BackgroundServiceAsyncExceptionGetsLogged()
backgroundDelayTaskSource.SetResult(true);
// give the background service 1 minute to log the failure
- TimeSpan timeout = TimeSpan.FromMinutes(1);
+ var timeout = TimeSpan.FromMinutes(1);
Stopwatch sw = Stopwatch.StartNew();
while (true)
{
- EventWrittenEventArgs[] events = listener.EventData.ToArray();
- if (events.Any(e =>
- e.EventSource.Name == "Microsoft-Extensions-Logging" &&
- e.Payload.OfType().Any(p => p.Contains("BackgroundService failed"))))
+ EventWrittenEventArgs[] events =
+ listener.EventData.Where(
+ e => e.EventSource.Name == "Microsoft-Extensions-Logging").ToArray();
+
+ if (expectedExceptionMessages.All(
+ expectedMessage => events.Any(
+ e => e.Payload.OfType().Any(
+ p => p.Contains(expectedMessage)))))
{
break;
}
@@ -1371,6 +1476,26 @@ public ValueTask DisposeAsync()
}
}
+ private class TestBackgroundService : IHostedService
+ {
+ private readonly Action _onStart;
+ private readonly Task _emulateWorkTask;
+
+ public TestBackgroundService(Task emulateWorkTask, Action onStart)
+ {
+ _emulateWorkTask = emulateWorkTask;
+ _onStart = onStart;
+ }
+
+ public async Task StartAsync(CancellationToken stoppingToken)
+ {
+ _onStart();
+ await _emulateWorkTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+ }
+
private class AsyncThrowingService : BackgroundService
{
private readonly Task _executeDelayTask;