diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/IApplicationLifetimeEvents.cs b/src/Microsoft.AspNetCore.Hosting.Abstractions/IApplicationLifetimeEvents.cs
new file mode 100644
index 0000000000..e3f5cb6b3a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/IApplicationLifetimeEvents.cs
@@ -0,0 +1,31 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Hosting
+{
+ ///
+ /// Allows consumers to perform cleanup during a graceful shutdown.
+ ///
+ public interface IApplicationLifetimeEvents
+ {
+ ///
+ /// Triggered when the application host has fully started and is about to wait
+ /// for a graceful shutdown.
+ ///
+ void OnApplicationStarted();
+
+ ///
+ /// Triggered when the application host is performing a graceful shutdown.
+ /// Requests may still be in flight. Shutdown will block until this event completes.
+ ///
+ void OnApplicationStopping();
+
+ ///
+ /// Triggered when the application host is performing a graceful shutdown.
+ /// All requests should be complete at this point. Shutdown will block
+ /// until this event completes.
+ ///
+ void OnApplicationStopped();
+
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/ApplicationLifetime.cs b/src/Microsoft.AspNetCore.Hosting/Internal/ApplicationLifetime.cs
index dbbf97d181..0517d46fad 100644
--- a/src/Microsoft.AspNetCore.Hosting/Internal/ApplicationLifetime.cs
+++ b/src/Microsoft.AspNetCore.Hosting/Internal/ApplicationLifetime.cs
@@ -2,7 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Hosting.Internal
{
@@ -14,6 +17,14 @@ namespace Microsoft.AspNetCore.Hosting.Internal
private readonly CancellationTokenSource _startedSource = new CancellationTokenSource();
private readonly CancellationTokenSource _stoppingSource = new CancellationTokenSource();
private readonly CancellationTokenSource _stoppedSource = new CancellationTokenSource();
+ private readonly IEnumerable _handlers = Enumerable.Empty();
+ private readonly ILogger _logger;
+
+ public ApplicationLifetime(ILogger logger, IEnumerable handlers)
+ {
+ _logger = logger;
+ _handlers = handlers;
+ }
///
/// Triggered when the application host has fully started and is about to wait
@@ -46,11 +57,13 @@ namespace Microsoft.AspNetCore.Hosting.Internal
{
try
{
- _stoppingSource.Cancel(throwOnFirstException: false);
+ ExecuteHandlers(_stoppingSource, handler => handler.OnApplicationStopping());
}
- catch (Exception)
+ catch (Exception ex)
{
- // TODO: LOG
+ _logger.ApplicationError(LoggerEventIds.ApplicationStoppingException,
+ "An error occurred stopping the application",
+ ex);
}
}
}
@@ -62,11 +75,13 @@ namespace Microsoft.AspNetCore.Hosting.Internal
{
try
{
- _startedSource.Cancel(throwOnFirstException: false);
+ ExecuteHandlers(_startedSource, handler => handler.OnApplicationStarted());
}
- catch (Exception)
+ catch (Exception ex)
{
- // TODO: LOG
+ _logger.ApplicationError(LoggerEventIds.ApplicationStartupException,
+ "An error occurred starting the application",
+ ex);
}
}
@@ -77,11 +92,63 @@ namespace Microsoft.AspNetCore.Hosting.Internal
{
try
{
- _stoppedSource.Cancel(throwOnFirstException: false);
+ ExecuteHandlers(_stoppedSource, handler => handler.OnApplicationStopped());
}
- catch (Exception)
+ catch (Exception ex)
{
- // TODO: LOG
+ _logger.ApplicationError(LoggerEventIds.ApplicationStoppedException,
+ "An error occurred stopping the application",
+ ex);
+ }
+ }
+
+ private void ExecuteHandlers(CancellationTokenSource cancel, Action callback)
+ {
+ // Noop if this is already cancelled
+ if (cancel.IsCancellationRequested)
+ {
+ return;
+ }
+
+ List exceptions = null;
+
+ try
+ {
+ // Run the cancellation token callbacks
+ cancel.Cancel(throwOnFirstException: false);
+ }
+ catch (Exception ex)
+ {
+ if (exceptions == null)
+ {
+ exceptions = new List();
+ }
+
+ exceptions.Add(ex);
+ }
+
+ // Run the handlers
+ foreach (var handler in _handlers)
+ {
+ try
+ {
+ callback(handler);
+ }
+ catch (Exception ex)
+ {
+ if (exceptions == null)
+ {
+ exceptions = new List();
+ }
+
+ exceptions.Add(ex);
+ }
+ }
+
+ // Throw an aggregate exception if there were any exceptions
+ if (exceptions != null)
+ {
+ throw new AggregateException(exceptions);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/HostingLoggerExtensions.cs b/src/Microsoft.AspNetCore.Hosting/Internal/HostingLoggerExtensions.cs
index ca0283a0aa..35072a3972 100644
--- a/src/Microsoft.AspNetCore.Hosting/Internal/HostingLoggerExtensions.cs
+++ b/src/Microsoft.AspNetCore.Hosting/Internal/HostingLoggerExtensions.cs
@@ -52,8 +52,14 @@ namespace Microsoft.AspNetCore.Hosting.Internal
public static void ApplicationError(this ILogger logger, Exception exception)
{
- var message = "Application startup exception";
+ logger.ApplicationError(
+ eventId: LoggerEventIds.ApplicationStartupException,
+ message: "Application startup exception",
+ exception: exception);
+ }
+ public static void ApplicationError(this ILogger logger, EventId eventId, string message, Exception exception)
+ {
var reflectionTypeLoadException = exception as ReflectionTypeLoadException;
if (reflectionTypeLoadException != null)
{
@@ -64,7 +70,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
}
logger.LogCritical(
- eventId: LoggerEventIds.ApplicationStartupException,
+ eventId: eventId,
message: message,
exception: exception);
}
diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/LoggerEventIds.cs b/src/Microsoft.AspNetCore.Hosting/Internal/LoggerEventIds.cs
index f2978b0d20..94764ec47f 100644
--- a/src/Microsoft.AspNetCore.Hosting/Internal/LoggerEventIds.cs
+++ b/src/Microsoft.AspNetCore.Hosting/Internal/LoggerEventIds.cs
@@ -11,5 +11,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
public const int Started = 4;
public const int Shutdown = 5;
public const int ApplicationStartupException = 6;
+ public const int ApplicationStoppingException = 7;
+ public const int ApplicationStoppedException = 8;
}
}
diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs b/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs
index 3a6b4f7562..440b17321d 100644
--- a/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs
+++ b/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs
@@ -7,7 +7,6 @@ using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
-using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Builder;
using Microsoft.AspNetCore.Hosting.Server;
@@ -28,9 +27,9 @@ namespace Microsoft.AspNetCore.Hosting.Internal
private readonly IServiceCollection _applicationServiceCollection;
private IStartup _startup;
+ private ApplicationLifetime _applicationLifetime;
private readonly IServiceProvider _hostingServiceProvider;
- private readonly ApplicationLifetime _applicationLifetime;
private readonly WebHostOptions _options;
private readonly IConfiguration _config;
@@ -68,8 +67,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
_options = options;
_applicationServiceCollection = appServices;
_hostingServiceProvider = hostingServiceProvider;
- _applicationLifetime = new ApplicationLifetime();
- _applicationServiceCollection.AddSingleton(_applicationLifetime);
+ _applicationServiceCollection.AddSingleton();
}
public IServiceProvider Services
@@ -101,6 +99,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
{
Initialize();
+ _applicationLifetime = _applicationServices.GetRequiredService() as ApplicationLifetime;
_logger = _applicationServices.GetRequiredService>();
var diagnosticSource = _applicationServices.GetRequiredService();
var httpContextFactory = _applicationServices.GetRequiredService();
@@ -109,7 +108,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
Server.Start(new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory));
- _applicationLifetime.NotifyStarted();
+ _applicationLifetime?.NotifyStarted();
_logger.Started();
}
@@ -244,10 +243,10 @@ namespace Microsoft.AspNetCore.Hosting.Internal
public void Dispose()
{
_logger?.Shutdown();
- _applicationLifetime.StopApplication();
+ _applicationLifetime?.StopApplication();
(_hostingServiceProvider as IDisposable)?.Dispose();
(_applicationServices as IDisposable)?.Dispose();
- _applicationLifetime.NotifyStopped();
+ _applicationLifetime?.NotifyStopped();
}
}
}
diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs
index ab52089b8f..44d0a720dc 100644
--- a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs
+++ b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs
@@ -270,6 +270,135 @@ namespace Microsoft.AspNetCore.Hosting
}
}
+ [Fact]
+ public void WebHostNotifiesAllIApplicationLifetimeCallbacksEvenIfTheyThrow()
+ {
+ var host = CreateBuilder()
+ .UseServer(this)
+ .Build();
+ var applicationLifetime = host.Services.GetService();
+
+ var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted);
+ var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
+ var stopped = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopped);
+
+ using (host)
+ {
+ host.Start();
+ Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested);
+ Assert.True(started.All(s => s));
+ host.Dispose();
+ Assert.True(stopping.All(s => s));
+ Assert.True(stopped.All(s => s));
+ }
+ }
+
+ [Fact]
+ public void WebHostNotifiesAllIApplicationLifetimeEventsCallbacksEvenIfTheyThrow()
+ {
+ bool[] events1 = null;
+ bool[] events2 = null;
+
+ var host = CreateBuilder()
+ .UseServer(this)
+ .ConfigureServices(services =>
+ {
+ events1 = RegisterCallbacksThatThrow(services);
+ events2 = RegisterCallbacksThatThrow(services);
+ })
+ .Build();
+
+ using (host)
+ {
+ host.Start();
+ Assert.True(events1[0]);
+ Assert.True(events2[0]);
+ host.Dispose();
+ Assert.True(events1[1]);
+ Assert.True(events2[1]);
+ Assert.True(events1[2]);
+ Assert.True(events2[2]);
+ }
+ }
+
+ [Fact]
+ public void WebHostStopCallbacksDontMultipleTimes()
+ {
+ var stoppingCalls = 0;
+ var stoppedCalls = 0;
+
+ var host = CreateBuilder()
+ .UseServer(this)
+ .ConfigureServices(services =>
+ {
+ Action started = () =>
+ {
+ };
+
+ Action stopping = () =>
+ {
+ stoppingCalls++;
+ };
+
+ Action stopped = () =>
+ {
+ stoppedCalls++;
+ };
+
+ services.AddSingleton(new DelegateLifetimeEvents(started, stopping, stopped));
+ })
+ .Build();
+ var lifetime = host.Services.GetRequiredService();
+ lifetime.StopApplication();
+ lifetime.StopApplication();
+ lifetime.StopApplication();
+
+ using (host)
+ {
+ host.Start();
+ host.Dispose();
+
+ Assert.Equal(1, stoppingCalls);
+ Assert.Equal(1, stoppedCalls);
+ }
+ }
+
+ [Fact]
+ public void WebHostNotifiesAllIApplicationLifetimeEventsCallbacksAndIApplicationLifetimeCallbacksEvenIfTheyThrow()
+ {
+ bool[] events1 = null;
+ bool[] events2 = null;
+
+ var host = CreateBuilder()
+ .UseServer(this)
+ .ConfigureServices(services =>
+ {
+ events1 = RegisterCallbacksThatThrow(services);
+ events2 = RegisterCallbacksThatThrow(services);
+ })
+ .Build();
+ var applicationLifetime = host.Services.GetService();
+
+ var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted);
+ var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
+ var stopped = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopped);
+
+ using (host)
+ {
+ host.Start();
+ Assert.True(events1[0]);
+ Assert.True(events2[0]);
+ Assert.True(started.All(s => s));
+ host.Dispose();
+ Assert.True(events1[1]);
+ Assert.True(events2[1]);
+ Assert.True(stopping.All(s => s));
+ Assert.True(events1[2]);
+ Assert.True(events2[2]);
+ Assert.True(stopped.All(s => s));
+ }
+ }
+
[Fact]
public void WebHostInjectsHostingEnvironment()
{
@@ -523,6 +652,48 @@ namespace Microsoft.AspNetCore.Hosting
return new WebHostBuilder().UseConfiguration(config ?? new ConfigurationBuilder().Build()).UseStartup("Microsoft.AspNetCore.Hosting.Tests");
}
+ private static bool[] RegisterCallbacksThatThrow(IServiceCollection services)
+ {
+ bool[] events = new bool[3];
+
+ Action started = () =>
+ {
+ events[0] = true;
+ throw new InvalidOperationException();
+ };
+
+ Action stopping = () =>
+ {
+ events[1] = true;
+ throw new InvalidOperationException();
+ };
+
+ Action stopped = () =>
+ {
+ events[2] = true;
+ throw new InvalidOperationException();
+ };
+
+ services.AddSingleton(new DelegateLifetimeEvents(started, stopping, stopped));
+
+ return events;
+ }
+
+ private static bool[] RegisterCallbacksThatThrow(CancellationToken token)
+ {
+ var signals = new bool[3];
+ for (int i = 0; i < signals.Length; i++)
+ {
+ token.Register(state =>
+ {
+ signals[(int)state] = true;
+ throw new InvalidOperationException();
+ }, i);
+ }
+
+ return signals;
+ }
+
public void Start(IHttpApplication application)
{
var startInstance = new StartInstance();
@@ -551,6 +722,26 @@ namespace Microsoft.AspNetCore.Hosting
}
}
+ private class DelegateLifetimeEvents : IApplicationLifetimeEvents
+ {
+ private readonly Action _started;
+ private readonly Action _stopping;
+ private readonly Action _stopped;
+
+ public DelegateLifetimeEvents(Action started, Action stopping, Action stopped)
+ {
+ _started = started;
+ _stopping = stopping;
+ _stopped = stopped;
+ }
+
+ public void OnApplicationStarted() => _started();
+
+ public void OnApplicationStopped() => _stopped();
+
+ public void OnApplicationStopping() => _stopping();
+ }
+
private class StartInstance : IDisposable
{
public int DisposeCalls { get; set; }