From 62f74d5be0b1f8091742d2d8a3e88f3fdbd05ab2 Mon Sep 17 00:00:00 2001 From: Chris R Date: Mon, 27 Mar 2017 10:30:45 -0700 Subject: [PATCH] #947 Add IServer.StopAsyc, IWebHost.StopAsync, and make Start async --- .../StartupExternallyControlled.cs | 5 +- ...ingAbstractionsWebHostBuilderExtensions.cs | 4 +- .../IWebHost.cs | 11 +- .../WebHostDefaults.cs | 2 + .../exceptions.net45.json | 19 + .../exceptions.netcore.json | 19 + .../IServer.cs | 11 +- .../exceptions.net45.json | 19 + .../exceptions.netcore.json | 19 + .../Internal/HostingLoggerExtensions.cs | 10 + .../Internal/LoggerEventIds.cs | 1 + .../Internal/WebHost.cs | 45 +- .../Internal/WebHostOptions.cs | 10 + .../WebHostExtensions.cs | 84 +++- .../TestServer.cs | 12 +- .../Program.cs | 13 +- .../WebHostBuilderTests.cs | 32 +- .../WebHostTests.cs | 408 ++++++++++++------ 18 files changed, 548 insertions(+), 176 deletions(-) diff --git a/samples/SampleStartups/StartupExternallyControlled.cs b/samples/SampleStartups/StartupExternallyControlled.cs index 69263877b7..218624bd65 100644 --- a/samples/SampleStartups/StartupExternallyControlled.cs +++ b/samples/SampleStartups/StartupExternallyControlled.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -34,8 +36,9 @@ namespace SampleStartups .Start(_urls.ToArray()); } - public void Stop() + public async Task StopAsync() { + await _host.StopAsync(TimeSpan.FromSeconds(5)); _host.Dispose(); } diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/HostingAbstractionsWebHostBuilderExtensions.cs b/src/Microsoft.AspNetCore.Hosting.Abstractions/HostingAbstractionsWebHostBuilderExtensions.cs index 0d4b659d94..c98de63e97 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/HostingAbstractionsWebHostBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/HostingAbstractionsWebHostBuilderExtensions.cs @@ -3,6 +3,8 @@ using System; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -166,7 +168,7 @@ namespace Microsoft.AspNetCore.Hosting public static IWebHost Start(this IWebHostBuilder hostBuilder, params string[] urls) { var host = hostBuilder.UseUrls(urls).Build(); - host.Start(); + host.StartAsync(CancellationToken.None).GetAwaiter().GetResult(); return host; } } diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/IWebHost.cs b/src/Microsoft.AspNetCore.Hosting.Abstractions/IWebHost.cs index 06796c4bf4..b1202d9405 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/IWebHost.cs +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/IWebHost.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Hosting @@ -24,6 +26,13 @@ namespace Microsoft.AspNetCore.Hosting /// /// Starts listening on the configured addresses. /// - void Start(); + Task StartAsync(CancellationToken cancellationToken); + + /// + /// Attempt to gracefully stop the host. + /// + /// + /// + Task StopAsync(CancellationToken cancellationToken); } } diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostDefaults.cs b/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostDefaults.cs index 98a10ebd0f..3d1dfd7ee1 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostDefaults.cs +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostDefaults.cs @@ -16,5 +16,7 @@ namespace Microsoft.AspNetCore.Hosting public static readonly string ServerUrlsKey = "urls"; public static readonly string ContentRootKey = "contentRoot"; public static readonly string PreferHostingUrls = "preferHostingUrls"; + + public static readonly string ShutdownTimeoutKey = "shutdownTimeoutSeconds"; } } diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/exceptions.net45.json b/src/Microsoft.AspNetCore.Hosting.Abstractions/exceptions.net45.json index 991748d863..8e5b3d3882 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/exceptions.net45.json +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/exceptions.net45.json @@ -16,5 +16,24 @@ "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHostBuilder", "NewMemberId": "Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureConfiguration(System.Action configureDelegate)", "Kind": "Addition" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "OldMemberId": "System.Void Start()", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken)", + "Kind": "Modification" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken)", + "Kind": "Addition" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken)", + "Kind": "Addition" } ] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/exceptions.netcore.json b/src/Microsoft.AspNetCore.Hosting.Abstractions/exceptions.netcore.json index 991748d863..8e5b3d3882 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/exceptions.netcore.json +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/exceptions.netcore.json @@ -16,5 +16,24 @@ "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHostBuilder", "NewMemberId": "Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureConfiguration(System.Action configureDelegate)", "Kind": "Addition" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "OldMemberId": "System.Void Start()", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken)", + "Kind": "Modification" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken)", + "Kind": "Addition" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHost : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken)", + "Kind": "Addition" } ] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/IServer.cs b/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/IServer.cs index 88ac8662af..b04e5a0d47 100644 --- a/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/IServer.cs +++ b/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/IServer.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Hosting.Server @@ -21,6 +23,13 @@ namespace Microsoft.AspNetCore.Hosting.Server /// /// An instance of . /// The context associated with the application. - void Start(IHttpApplication application); + /// Indicates if the server startup should be aborted. + Task StartAsync(IHttpApplication application, CancellationToken cancellationToken); + + /// + /// Stop processing requests and shut down the server, gracefully if possible. + /// + /// Indicates if the graceful shutdown should be aborted. + Task StopAsync(CancellationToken cancellationToken); } } diff --git a/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/exceptions.net45.json b/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/exceptions.net45.json index b26478ae79..7a5789b8f7 100644 --- a/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/exceptions.net45.json +++ b/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/exceptions.net45.json @@ -10,5 +10,24 @@ "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature", "NewMemberId": "System.Void set_PreferHostingUrls(System.Boolean value)", "Kind": "Addition" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "OldMemberId": "System.Void Start(Microsoft.AspNetCore.Hosting.Server.IHttpApplication application)", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StartAsync(Microsoft.AspNetCore.Hosting.Server.IHttpApplication application, System.Threading.CancellationToken cancellationToken)", + "Kind": "Modification" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StartAsync(Microsoft.AspNetCore.Hosting.Server.IHttpApplication application, System.Threading.CancellationToken cancellationToken)", + "Kind": "Addition" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken)", + "Kind": "Addition" } ] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/exceptions.netcore.json b/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/exceptions.netcore.json index b26478ae79..7a5789b8f7 100644 --- a/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/exceptions.netcore.json +++ b/src/Microsoft.AspNetCore.Hosting.Server.Abstractions/exceptions.netcore.json @@ -10,5 +10,24 @@ "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature", "NewMemberId": "System.Void set_PreferHostingUrls(System.Boolean value)", "Kind": "Addition" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "OldMemberId": "System.Void Start(Microsoft.AspNetCore.Hosting.Server.IHttpApplication application)", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StartAsync(Microsoft.AspNetCore.Hosting.Server.IHttpApplication application, System.Threading.CancellationToken cancellationToken)", + "Kind": "Modification" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StartAsync(Microsoft.AspNetCore.Hosting.Server.IHttpApplication application, System.Threading.CancellationToken cancellationToken)", + "Kind": "Addition" + }, + { + "OldTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable", + "NewMemberId": "System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken)", + "Kind": "Addition" } ] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/HostingLoggerExtensions.cs b/src/Microsoft.AspNetCore.Hosting/Internal/HostingLoggerExtensions.cs index b05e70eb62..987924d8e6 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/HostingLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/HostingLoggerExtensions.cs @@ -82,6 +82,16 @@ namespace Microsoft.AspNetCore.Hosting.Internal } } + public static void ServerShutdownException(this ILogger logger, Exception ex) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.ServerShutdownException, + exception: ex, + message: "Server shutdown exception"); + } + } private class HostingLogScope : IReadOnlyList> { diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/LoggerEventIds.cs b/src/Microsoft.AspNetCore.Hosting/Internal/LoggerEventIds.cs index c31cae0623..f7d8f61933 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/LoggerEventIds.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/LoggerEventIds.cs @@ -16,5 +16,6 @@ namespace Microsoft.AspNetCore.Hosting.Internal public const int HostedServiceStartException = 9; public const int HostedServiceStopException = 10; public const int HostingStartupAssemblyException = 11; + public const int ServerShutdownException = 12; } } diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs b/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs index 52e09b7591..d09620344f 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs @@ -7,6 +7,8 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Builder; using Microsoft.AspNetCore.Hosting.Server; @@ -39,6 +41,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal private RequestDelegate _application; private ILogger _logger; + private bool _stopped; + // Used for testing only internal WebHostOptions Options => _options; @@ -100,7 +104,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal } } - public virtual void Start() + public virtual async Task StartAsync(CancellationToken cancellationToken) { HostingEventSource.Log.HostStart(); _logger = _applicationServices.GetRequiredService>(); @@ -112,7 +116,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal _hostedServiceExecutor = _applicationServices.GetRequiredService(); var diagnosticSource = _applicationServices.GetRequiredService(); var httpContextFactory = _applicationServices.GetRequiredService(); - Server.Start(new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory)); + var hostingApp = new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory); + await Server.StartAsync(hostingApp, cancellationToken); // Fire IApplicationLifetime.Started _applicationLifetime?.NotifyStarted(); @@ -267,23 +272,51 @@ namespace Microsoft.AspNetCore.Hosting.Internal } } - public void Dispose() + public async Task StopAsync(CancellationToken cancellationToken) { + if (_stopped) + { + return; + } + _stopped = true; + _logger?.Shutdown(); + if (!cancellationToken.CanBeCanceled) + { + cancellationToken = new CancellationTokenSource(Options.ShutdownTimeout).Token; + } + // Fire IApplicationLifetime.Stopping _applicationLifetime?.StopApplication(); + await Server?.StopAsync(cancellationToken); + // Fire the IHostedService.Stop _hostedServiceExecutor?.Stop(); - (_hostingServiceProvider as IDisposable)?.Dispose(); - (_applicationServices as IDisposable)?.Dispose(); - // Fire IApplicationLifetime.Stopped _applicationLifetime?.NotifyStopped(); HostingEventSource.Log.HostStop(); } + + public void Dispose() + { + if (!_stopped) + { + try + { + this.StopAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + _logger?.ServerShutdownException(ex); + } + } + + (_applicationServices as IDisposable)?.Dispose(); + (_hostingServiceProvider as IDisposable)?.Dispose(); + } } } diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/WebHostOptions.cs b/src/Microsoft.AspNetCore.Hosting/Internal/WebHostOptions.cs index 0a2f216e9a..9e960d0b03 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/WebHostOptions.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/WebHostOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using Microsoft.Extensions.Configuration; namespace Microsoft.AspNetCore.Hosting.Internal @@ -27,6 +28,13 @@ namespace Microsoft.AspNetCore.Hosting.Internal ContentRootPath = configuration[WebHostDefaults.ContentRootKey]; HostingStartupAssemblies = configuration[WebHostDefaults.HostingStartupAssembliesKey]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0]; PreferHostingUrls = ParseBool(configuration, WebHostDefaults.PreferHostingUrls); + + var timeout = configuration[WebHostDefaults.ShutdownTimeoutKey]; + if (!string.IsNullOrEmpty(timeout) + && int.TryParse(timeout, NumberStyles.None, CultureInfo.InvariantCulture, out var seconds)) + { + ShutdownTimeout = TimeSpan.FromSeconds(seconds); + } } public string ApplicationName { get; set; } @@ -47,6 +55,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal public bool PreferHostingUrls { get; set; } + public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + private static bool ParseBool(IConfiguration configuration, string key) { return string.Equals("true", configuration[key], StringComparison.OrdinalIgnoreCase) diff --git a/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs b/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs index 4fdbbf6025..37f139c7fd 100644 --- a/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs +++ b/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Runtime.Loader; #endif using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.DependencyInjection; @@ -14,11 +15,62 @@ namespace Microsoft.AspNetCore.Hosting { public static class WebHostExtensions { + /// + /// Starts the host. + /// + /// + /// + public static void Start(this IWebHost host) + { + host.StartAsync().GetAwaiter().GetResult(); + } + + /// + /// Starts the host. + /// + /// + /// + public static Task StartAsync(this IWebHost host) + { + return host.StartAsync(CancellationToken.None); + } + + /// + /// Gracefully stops the host. + /// + /// + /// + public static Task StopAsync(this IWebHost host) + { + return host.StopAsync(CancellationToken.None); + } + + /// + /// Attempts to gracefully stop the host with the given timeout. + /// + /// + /// The timeout for stopping gracefully. Once expired the + /// server may terminate any remaining active connections. + /// + public static Task StopAsync(this IWebHost host, TimeSpan timeout) + { + return host.StopAsync(new CancellationTokenSource(timeout).Token); + } + /// /// Runs a web application and block the calling thread until host shutdown. /// /// The to run. public static void Run(this IWebHost host) + { + host.RunAsync().GetAwaiter().GetResult(); + } + + /// + /// Runs a web application and returns a Task that only completes on host shutdown. + /// + /// The to run. + public static async Task RunAsync(this IWebHost host) { var done = new ManualResetEventSlim(false); using (var cts = new CancellationTokenSource()) @@ -28,7 +80,13 @@ namespace Microsoft.AspNetCore.Hosting if (!cts.IsCancellationRequested) { Console.WriteLine("Application is shutting down..."); - cts.Cancel(); + try + { + cts.Cancel(); + } + catch (ObjectDisposedException) + { + } } done.Wait(); @@ -48,26 +106,26 @@ namespace Microsoft.AspNetCore.Hosting eventArgs.Cancel = true; }; - host.Run(cts.Token, "Application started. Press Ctrl+C to shut down."); + await host.RunAsync(cts.Token, "Application started. Press Ctrl+C to shut down."); done.Set(); } } /// - /// Runs a web application and block the calling thread until token is triggered or shutdown is triggered. + /// Runs a web application and and returns a Task that only completes when the token is triggered or shutdown is triggered. /// /// The to run. /// The token to trigger shutdown. - public static void Run(this IWebHost host, CancellationToken token) + public static Task RunAsync(this IWebHost host, CancellationToken token) { - host.Run(token, shutdownMessage: null); + return host.RunAsync(token, shutdownMessage: null); } - private static void Run(this IWebHost host, CancellationToken token, string shutdownMessage) + private static async Task RunAsync(this IWebHost host, CancellationToken token, string shutdownMessage) { using (host) { - host.Start(); + await host.StartAsync(token); var hostingEnvironment = host.Services.GetService(); var applicationLifetime = host.Services.GetService(); @@ -95,7 +153,17 @@ namespace Microsoft.AspNetCore.Hosting }, applicationLifetime); - applicationLifetime.ApplicationStopping.WaitHandle.WaitOne(); + var waitForStop = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + applicationLifetime.ApplicationStopping.Register(obj => + { + var tcs = (TaskCompletionSource)obj; + tcs.TrySetResult(null); + }, waitForStop); + + await waitForStop.Task; + + // WebHost will use its default ShutdownTimeout if none is specified. + await host.StopAsync(); } } } diff --git a/src/Microsoft.AspNetCore.TestHost/TestServer.cs b/src/Microsoft.AspNetCore.TestHost/TestServer.cs index 9d7ab9f4a7..7ae6066eb5 100644 --- a/src/Microsoft.AspNetCore.TestHost/TestServer.cs +++ b/src/Microsoft.AspNetCore.TestHost/TestServer.cs @@ -3,6 +3,7 @@ using System; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; @@ -39,7 +40,7 @@ namespace Microsoft.AspNetCore.TestHost Features = featureCollection; var host = builder.UseServer(this).Build(); - host.Start(); + host.StartAsync().GetAwaiter().GetResult(); _hostInstance = host; } @@ -91,7 +92,7 @@ namespace Microsoft.AspNetCore.TestHost } } - void IServer.Start(IHttpApplication application) + Task IServer.StartAsync(IHttpApplication application, CancellationToken cancellationToken) { _application = new ApplicationWrapper((IHttpApplication)application, () => { @@ -100,6 +101,13 @@ namespace Microsoft.AspNetCore.TestHost throw new ObjectDisposedException(GetType().FullName); } }); + + return Task.CompletedTask; + } + + Task IServer.StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; } private class ApplicationWrapper : IHttpApplication diff --git a/test/Microsoft.AspNetCore.Hosting.TestSites/Program.cs b/test/Microsoft.AspNetCore.Hosting.TestSites/Program.cs index 33332152e9..57852ba997 100644 --- a/test/Microsoft.AspNetCore.Hosting.TestSites/Program.cs +++ b/test/Microsoft.AspNetCore.Hosting.TestSites/Program.cs @@ -1,11 +1,12 @@ // 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. -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Configuration; namespace ServerComparison.TestSites { @@ -36,8 +37,14 @@ namespace ServerComparison.TestSites public IFeatureCollection Features { get; } = new FeatureCollection(); - public void Start(IHttpApplication application) + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; } } } diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs index 36211ed450..7d9934a8a9 100644 --- a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs +++ b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -48,7 +49,7 @@ namespace Microsoft.AspNetCore.Hosting var host = builder.UseServer(server).UseStartup("MissingStartupAssembly").Build(); using (host) { - host.Start(); + await host.StartAsync(); await AssertResponseContains(server.RequestDelegate, "MissingStartupAssembly"); } } @@ -61,7 +62,7 @@ namespace Microsoft.AspNetCore.Hosting var host = builder.UseServer(server).UseStartup().Build(); using (host) { - host.Start(); + await host.StartAsync(); await AssertResponseContains(server.RequestDelegate, "Exception from static constructor"); } } @@ -74,7 +75,7 @@ namespace Microsoft.AspNetCore.Hosting var host = builder.UseServer(server).UseStartup().Build(); using (host) { - host.Start(); + await host.StartAsync(); await AssertResponseContains(server.RequestDelegate, "Exception from constructor"); } } @@ -87,7 +88,7 @@ namespace Microsoft.AspNetCore.Hosting var host = builder.UseServer(server).UseStartup().Build(); using (host) { - host.Start(); + await host.StartAsync(); await AssertResponseContains(server.RequestDelegate, "Message from the LoaderException"); } } @@ -100,7 +101,7 @@ namespace Microsoft.AspNetCore.Hosting var host = builder.UseServer(server).UseStartup().Build(); using (host) { - host.Start(); + await host.StartAsync(); var services = host.Services.GetServices(); Assert.NotNull(services); Assert.NotEmpty(services); @@ -110,7 +111,7 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void DefaultObjectPoolProvider_IsRegistered() + public async Task DefaultObjectPoolProvider_IsRegistered() { var server = new TestServer(); var host = CreateWebHostBuilder() @@ -119,7 +120,7 @@ namespace Microsoft.AspNetCore.Hosting .Build(); using (host) { - host.Start(); + await host.StartAsync(); Assert.IsType(host.Services.GetService()); } } @@ -132,7 +133,7 @@ namespace Microsoft.AspNetCore.Hosting var host = builder.UseServer(server).UseStartup().Build(); using (host) { - host.Start(); + await host.StartAsync(); await AssertResponseContains(server.RequestDelegate, "Exception from ConfigureServices"); } } @@ -145,7 +146,7 @@ namespace Microsoft.AspNetCore.Hosting var host = builder.UseServer(server).UseStartup().Build(); using (host) { - host.Start(); + await host.StartAsync(); await AssertResponseContains(server.RequestDelegate, "Exception from Configure"); } } @@ -810,7 +811,7 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void Build_DoesNotThrowIfUnloadableAssemblyNameInHostingStartupAssembliesAndCaptureStartupErrorsTrue() + public async Task Build_DoesNotThrowIfUnloadableAssemblyNameInHostingStartupAssembliesAndCaptureStartupErrorsTrue() { var provider = new TestLoggerProvider(); var builder = CreateWebHostBuilder() @@ -825,7 +826,7 @@ namespace Microsoft.AspNetCore.Hosting using (var host = builder.Build()) { - host.Start(); + await host.StartAsync(); var context = provider.Sink.Writes.FirstOrDefault(s => s.EventId.Id == LoggerEventIds.HostingStartupAssemblyException); Assert.NotNull(context); } @@ -879,7 +880,7 @@ namespace Microsoft.AspNetCore.Hosting } - public void Start(IHttpApplication application) + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) { RequestDelegate = async ctx => { @@ -895,6 +896,13 @@ namespace Microsoft.AspNetCore.Hosting } application.DisposeContext(httpContext, null); }; + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; } } diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs index 6a6c833c86..fb6b8fc007 100644 --- a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs +++ b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs @@ -23,40 +23,12 @@ using Xunit; namespace Microsoft.AspNetCore.Hosting { - public class WebHostTests : IServer + public class WebHostTests { - private readonly IList _startInstances = new List(); - private IFeatureCollection _featuresSupportedByThisHost = NewFeatureCollection(); - - public IFeatureCollection Features - { - get - { - var features = new FeatureCollection(); - - foreach (var feature in _featuresSupportedByThisHost) - { - features[feature.Key] = feature.Value; - } - - return features; - } - } - - static IFeatureCollection NewFeatureCollection() - { - var stub = new StubFeatures(); - var features = new FeatureCollection(); - features.Set(stub); - features.Set(stub); - features.Set(new ServerAddressesFeature()); - return features; - } - [Fact] - public void WebHostThrowsWithNoServer() + public async Task WebHostThrowsWithNoServer() { - var ex = Assert.Throws(() => CreateBuilder().Build().Start()); + var ex = await Assert.ThrowsAsync(() => CreateBuilder().Build().StartAsync()); Assert.Equal("No service for type 'Microsoft.AspNetCore.Hosting.Server.IServer' has been registered.", ex.Message); } @@ -67,11 +39,11 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void NoDefaultAddressesAndDoNotPreferHostingUrlsIfNotConfigured() + public async Task NoDefaultAddressesAndDoNotPreferHostingUrlsIfNotConfigured() { - using (var host = CreateBuilder().UseServer(this).Build()) + using (var host = CreateBuilder().UseFakeServer().Build()) { - host.Start(); + await host.StartAsync(); var serverAddressesFeature = host.ServerFeatures.Get(); Assert.False(serverAddressesFeature.Addresses.Any()); Assert.False(serverAddressesFeature.PreferHostingUrls); @@ -79,7 +51,7 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void UsesLegacyConfigurationForAddressesAndDoNotPreferHostingUrlsIfNotConfigured() + public async Task UsesLegacyConfigurationForAddressesAndDoNotPreferHostingUrlsIfNotConfigured() { var data = new Dictionary { @@ -88,9 +60,9 @@ namespace Microsoft.AspNetCore.Hosting var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); - using (var host = CreateBuilder(config).UseServer(this).Build()) + using (var host = CreateBuilder(config).UseFakeServer().Build()) { - host.Start(); + await host.StartAsync(); var serverAddressFeature = host.ServerFeatures.Get(); Assert.Equal("http://localhost:5002", serverAddressFeature.Addresses.First()); Assert.False(serverAddressFeature.PreferHostingUrls); @@ -107,7 +79,7 @@ namespace Microsoft.AspNetCore.Hosting var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); - using (var host = CreateBuilder(config).UseServer(this).Build()) + using (var host = CreateBuilder(config).UseFakeServer().Build()) { host.Start(); var serverAddressFeature = host.ServerFeatures.Get(); @@ -117,7 +89,7 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void UsesNewConfigurationOverLegacyConfigForAddressesAndDoNotPreferHostingUrlsIfNotConfigured() + public async Task UsesNewConfigurationOverLegacyConfigForAddressesAndDoNotPreferHostingUrlsIfNotConfigured() { var data = new Dictionary { @@ -127,9 +99,9 @@ namespace Microsoft.AspNetCore.Hosting var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); - using (var host = CreateBuilder(config).UseServer(this).Build()) + using (var host = CreateBuilder(config).UseFakeServer().Build()) { - host.Start(); + await host.StartAsync(); var serverAddressFeature = host.ServerFeatures.Get(); Assert.Equal("http://localhost:5009", serverAddressFeature.Addresses.First()); Assert.False(serverAddressFeature.PreferHostingUrls); @@ -139,7 +111,7 @@ namespace Microsoft.AspNetCore.Hosting [Fact] public void DoNotPreferHostingUrlsWhenNoAddressConfigured() { - using (var host = CreateBuilder().UseServer(this).PreferHostingUrls(true).Build()) + using (var host = CreateBuilder().UseFakeServer().PreferHostingUrls(true).Build()) { host.Start(); var serverAddressesFeature = host.ServerFeatures.Get(); @@ -149,7 +121,7 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void PreferHostingUrlsWhenAddressIsConfigured() + public async Task PreferHostingUrlsWhenAddressIsConfigured() { var data = new Dictionary { @@ -158,9 +130,9 @@ namespace Microsoft.AspNetCore.Hosting var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); - using (var host = CreateBuilder(config).UseServer(this).PreferHostingUrls(true).Build()) + using (var host = CreateBuilder(config).UseFakeServer().PreferHostingUrls(true).Build()) { - host.Start(); + await host.StartAsync(); Assert.True(host.ServerFeatures.Get().PreferHostingUrls); } } @@ -169,17 +141,18 @@ namespace Microsoft.AspNetCore.Hosting public void WebHostCanBeStarted() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .UseStartup("Microsoft.AspNetCore.Hosting.Tests") .Start()) { + var server = (FakeServer)host.Services.GetRequiredService(); Assert.NotNull(host); - Assert.Equal(1, _startInstances.Count); - Assert.Equal(0, _startInstances[0].DisposeCalls); + Assert.Equal(1, server.StartInstances.Count); + Assert.Equal(0, server.StartInstances[0].DisposeCalls); host.Dispose(); - Assert.Equal(0, _startInstances[0].DisposeCalls); + Assert.Equal(1, server.StartInstances[0].DisposeCalls); } } @@ -187,28 +160,29 @@ namespace Microsoft.AspNetCore.Hosting public void WebHostShutsDownWhenTokenTriggers() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .UseStartup("Microsoft.AspNetCore.Hosting.Tests") .Build()) { var lifetime = host.Services.GetRequiredService(); + var server = (FakeServer)host.Services.GetRequiredService(); var cts = new CancellationTokenSource(); - Task.Run(() => host.Run(cts.Token)); + var runInBackground = host.RunAsync(cts.Token); // Wait on the host to be started lifetime.ApplicationStarted.WaitHandle.WaitOne(); - Assert.Equal(1, _startInstances.Count); - Assert.Equal(0, _startInstances[0].DisposeCalls); + Assert.Equal(1, server.StartInstances.Count); + Assert.Equal(0, server.StartInstances[0].DisposeCalls); cts.Cancel(); // Wait on the host to shutdown lifetime.ApplicationStopped.WaitHandle.WaitOne(); - Assert.Equal(0, _startInstances[0].DisposeCalls); + Assert.Equal(1, server.StartInstances[0].DisposeCalls); } } @@ -216,7 +190,7 @@ namespace Microsoft.AspNetCore.Hosting public void WebHostApplicationLifetimeEventsOrderedCorrectlyDuringShutdown() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .UseStartup("Microsoft.AspNetCore.Hosting.Tests") .Build()) { @@ -251,9 +225,9 @@ namespace Microsoft.AspNetCore.Hosting applicationStoppedEvent.Set(); }); - var runHostAndVerifyApplicationStopped = Task.Run(() => + var runHostAndVerifyApplicationStopped = Task.Run(async () => { - host.Run(); + await host.RunAsync(); // Check whether the applicationStoppingEvent has been set applicationStoppedCompletedBeforeRunCompleted = applicationStoppedEvent.IsSet; }); @@ -275,10 +249,10 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void WebHostDisposesServiceProvider() + public async Task WebHostDisposesServiceProvider() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .ConfigureServices(s => { s.AddTransient(); @@ -287,7 +261,7 @@ namespace Microsoft.AspNetCore.Hosting .UseStartup("Microsoft.AspNetCore.Hosting.Tests") .Build()) { - host.Start(); + await host.StartAsync(); var singleton = (FakeService)host.Services.GetService(); var transient = (FakeService)host.Services.GetService(); @@ -295,6 +269,11 @@ namespace Microsoft.AspNetCore.Hosting Assert.False(singleton.Disposed); Assert.False(transient.Disposed); + await host.StopAsync(); + + Assert.False(singleton.Disposed); + Assert.False(transient.Disposed); + host.Dispose(); Assert.True(singleton.Disposed); @@ -303,26 +282,26 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void WebHostNotifiesApplicationStarted() + public async Task WebHostNotifiesApplicationStarted() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .Build()) { var applicationLifetime = host.Services.GetService(); Assert.False(applicationLifetime.ApplicationStarted.IsCancellationRequested); - host.Start(); + await host.StartAsync(); Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested); } } [Fact] - public void WebHostNotifiesAllIApplicationLifetimeCallbacksEvenIfTheyThrow() + public async Task WebHostNotifiesAllIApplicationLifetimeCallbacksEvenIfTheyThrow() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .Build()) { var applicationLifetime = host.Services.GetService(); @@ -331,7 +310,7 @@ namespace Microsoft.AspNetCore.Hosting var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); var stopped = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopped); - host.Start(); + await host.StartAsync(); Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested); Assert.True(started.All(s => s)); host.Dispose(); @@ -341,13 +320,13 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void WebHostNotifiesAllIApplicationLifetimeEventsCallbacksEvenIfTheyThrow() + public async Task WebHostNotifiesAllIApplicationLifetimeEventsCallbacksEvenIfTheyThrow() { bool[] events1 = null; bool[] events2 = null; using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .ConfigureServices(services => { events1 = RegisterCallbacksThatThrow(services); @@ -355,7 +334,7 @@ namespace Microsoft.AspNetCore.Hosting }) .Build()) { - host.Start(); + await host.StartAsync(); Assert.True(events1[0]); Assert.True(events2[0]); host.Dispose(); @@ -365,12 +344,13 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void WebHostStopApplicationDoesNotFireStopOnHostedService() + public async Task WebHostStopApplicationDoesNotFireStopOnHostedService() { var stoppingCalls = 0; + var disposingCalls = 0; using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .ConfigureServices(services => { Action started = () => @@ -382,24 +362,32 @@ namespace Microsoft.AspNetCore.Hosting stoppingCalls++; }; - services.AddSingleton(new DelegateHostedService(started, stopping)); + Action disposing = () => + { + disposingCalls++; + }; + + services.AddSingleton(_ => new DelegateHostedService(started, stopping, disposing)); }) .Build()) { var lifetime = host.Services.GetRequiredService(); lifetime.StopApplication(); - host.Start(); + await host.StartAsync(); Assert.Equal(0, stoppingCalls); + Assert.Equal(0, disposingCalls); } + Assert.Equal(1, stoppingCalls); + Assert.Equal(1, disposingCalls); } [Fact] - public void HostedServiceCanInjectApplicationLifetime() + public async Task HostedServiceCanInjectApplicationLifetime() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .ConfigureServices(services => { services.AddSingleton(); @@ -409,19 +397,21 @@ namespace Microsoft.AspNetCore.Hosting var lifetime = host.Services.GetRequiredService(); lifetime.StopApplication(); - host.Start(); + await host.StartAsync(); var svc = (TestHostedService)host.Services.GetRequiredService(); Assert.True(svc.StartCalled); - host.Dispose(); + + await host.StopAsync(); Assert.True(svc.StopCalled); + host.Dispose(); } } [Fact] - public void HostedServiceStartNotCalledIfWebHostNotStarted() + public async Task HostedServiceStartNotCalledIfWebHostNotStarted() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .ConfigureServices(services => { services.AddSingleton(); @@ -433,19 +423,23 @@ namespace Microsoft.AspNetCore.Hosting var svc = (TestHostedService)host.Services.GetRequiredService(); Assert.False(svc.StartCalled); + await host.StopAsync(); + Assert.False(svc.StopCalled); host.Dispose(); Assert.False(svc.StopCalled); + Assert.True(svc.DisposeCalled); } } [Fact] - public void WebHostDisposeApplicationFiresStopOnHostedService() + public async Task WebHostStopApplicationFiresStopOnHostedService() { var stoppingCalls = 0; var startedCalls = 0; + var disposingCalls = 0; using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .ConfigureServices(services => { Action started = () => @@ -458,28 +452,90 @@ namespace Microsoft.AspNetCore.Hosting stoppingCalls++; }; - services.AddSingleton(new DelegateHostedService(started, stopping)); + Action disposing = () => + { + disposingCalls++; + }; + + services.AddSingleton(_ => new DelegateHostedService(started, stopping, disposing)); }) .Build()) { var lifetime = host.Services.GetRequiredService(); - host.Start(); + Assert.Equal(0, startedCalls); + + await host.StartAsync(); + Assert.Equal(1, startedCalls); + Assert.Equal(0, stoppingCalls); + Assert.Equal(0, disposingCalls); + + await host.StopAsync(); + + Assert.Equal(1, startedCalls); + Assert.Equal(1, stoppingCalls); + Assert.Equal(0, disposingCalls); + host.Dispose(); Assert.Equal(1, startedCalls); Assert.Equal(1, stoppingCalls); + Assert.Equal(1, disposingCalls); } } [Fact] - public void WebHostNotifiesAllIHostedServicesAndIApplicationLifetimeCallbacksEvenIfTheyThrow() + public async Task WebHostDisposeApplicationFiresStopOnHostedService() + { + var stoppingCalls = 0; + var startedCalls = 0; + var disposingCalls = 0; + + using (var host = CreateBuilder() + .UseFakeServer() + .ConfigureServices(services => + { + Action started = () => + { + startedCalls++; + }; + + Action stopping = () => + { + stoppingCalls++; + }; + + Action disposing = () => + { + disposingCalls++; + }; + + services.AddSingleton(_ => new DelegateHostedService(started, stopping, disposing)); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService(); + + Assert.Equal(0, startedCalls); + await host.StartAsync(); + Assert.Equal(1, startedCalls); + Assert.Equal(0, stoppingCalls); + Assert.Equal(0, disposingCalls); + host.Dispose(); + + Assert.Equal(1, stoppingCalls); + Assert.Equal(1, disposingCalls); + } + } + + [Fact] + public async Task WebHostNotifiesAllIHostedServicesAndIApplicationLifetimeCallbacksEvenIfTheyThrow() { bool[] events1 = null; bool[] events2 = null; using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .ConfigureServices(services => { events1 = RegisterCallbacksThatThrow(services); @@ -492,7 +548,7 @@ namespace Microsoft.AspNetCore.Hosting var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted); var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); - host.Start(); + await host.StartAsync(); Assert.True(events1[0]); Assert.True(events2[0]); Assert.True(started.All(s => s)); @@ -504,15 +560,15 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void WebHostInjectsHostingEnvironment() + public async Task WebHostInjectsHostingEnvironment() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .UseStartup("Microsoft.AspNetCore.Hosting.Tests") .UseEnvironment("WithHostingEnvironment") .Build()) { - host.Start(); + await host.StartAsync(); var env = host.Services.GetService(); Assert.Equal("Changed", env.EnvironmentName); } @@ -526,7 +582,7 @@ namespace Microsoft.AspNetCore.Hosting { services.AddTransient(); }) - .UseServer(this) + .UseFakeServer() .UseStartup("Microsoft.AspNetCore.Hosting.Tests"); Assert.Throws(() => builder.Build()); @@ -535,7 +591,7 @@ namespace Microsoft.AspNetCore.Hosting [Fact] public void CanCreateApplicationServicesWithAddedServices() { - using (var host = CreateBuilder().UseServer(this).ConfigureServices(services => services.AddOptions()).Build()) + using (var host = CreateBuilder().UseFakeServer().ConfigureServices(services => services.AddOptions()).Build()) { Assert.NotNull(host.Services.GetRequiredService>()); } @@ -547,7 +603,7 @@ namespace Microsoft.AspNetCore.Hosting // Verify ordering var configureOrder = 0; using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .ConfigureServices(services => { services.AddTransient(serviceProvider => new TestFilter( @@ -593,7 +649,7 @@ namespace Microsoft.AspNetCore.Hosting [Fact] public void EnvDefaultsToProductionIfNoConfig() { - using (var host = CreateBuilder().UseServer(this).Build()) + using (var host = CreateBuilder().UseFakeServer().Build()) { var env = host.Services.GetService(); Assert.Equal(EnvironmentName.Production, env.EnvironmentName); @@ -612,7 +668,7 @@ namespace Microsoft.AspNetCore.Hosting .AddInMemoryCollection(vals); var config = builder.Build(); - using (var host = CreateBuilder(config).UseServer(this).Build()) + using (var host = CreateBuilder(config).UseFakeServer().Build()) { var env = host.Services.GetService(); Assert.Equal("Staging", env.EnvironmentName); @@ -631,7 +687,7 @@ namespace Microsoft.AspNetCore.Hosting .AddInMemoryCollection(vals); var config = builder.Build(); - using (var host = CreateBuilder(config).UseServer(this).Build()) + using (var host = CreateBuilder(config).UseFakeServer().Build()) { var env = host.Services.GetService(); Assert.Equal(Path.GetFullPath("testroot"), env.WebRootPath); @@ -640,11 +696,11 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void IsEnvironment_Extension_Is_Case_Insensitive() + public async Task IsEnvironment_Extension_Is_Case_Insensitive() { - using (var host = CreateBuilder().UseServer(this).Build()) + using (var host = CreateBuilder().UseFakeServer().Build()) { - host.Start(); + await host.StartAsync(); var env = host.Services.GetRequiredService(); Assert.True(env.IsEnvironment(EnvironmentName.Production)); Assert.True(env.IsEnvironment("producTion")); @@ -652,7 +708,7 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void WebHost_CreatesDefaultRequestIdentifierFeature_IfNotPresent() + public async Task WebHost_CreatesDefaultRequestIdentifierFeature_IfNotPresent() { // Arrange HttpContext httpContext = null; @@ -665,7 +721,7 @@ namespace Microsoft.AspNetCore.Hosting using (var host = CreateHost(requestDelegate)) { // Act - host.Start(); + await host.StartAsync(); // Assert Assert.NotNull(httpContext); @@ -676,7 +732,7 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void WebHost_DoesNot_CreateDefaultRequestIdentifierFeature_IfPresent() + public async Task WebHost_DoesNot_CreateDefaultRequestIdentifierFeature_IfPresent() { // Arrange HttpContext httpContext = null; @@ -686,12 +742,18 @@ namespace Microsoft.AspNetCore.Hosting return Task.FromResult(0); }); var requestIdentifierFeature = new StubHttpRequestIdentifierFeature(); - _featuresSupportedByThisHost[typeof(IHttpRequestIdentifierFeature)] = requestIdentifierFeature; using (var host = CreateHost(requestDelegate)) { + var server = (FakeServer)host.Services.GetRequiredService(); + server.CreateRequestFeatures = () => + { + var features = FakeServer.NewFeatureCollection(); + features.Set(requestIdentifierFeature); + return features; + }; // Act - host.Start(); + await host.StartAsync(); // Assert Assert.NotNull(httpContext); @@ -700,14 +762,14 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void WebHost_InvokesConfigureMethodsOnlyOnce() + public async Task WebHost_InvokesConfigureMethodsOnlyOnce() { using (var host = CreateBuilder() - .UseServer(this) + .UseFakeServer() .UseStartup() .Build()) { - host.Start(); + await host.StartAsync(); var services = host.Services; var services2 = host.Services; Assert.Equal(1, CountStartup.ConfigureCount); @@ -735,7 +797,7 @@ namespace Microsoft.AspNetCore.Hosting public void WebHost_ThrowsForBadConfigureServiceSignature() { var builder = CreateBuilder() - .UseServer(this) + .UseFakeServer() .UseStartup(); var ex = Assert.Throws(() => builder.Build()); @@ -751,7 +813,7 @@ namespace Microsoft.AspNetCore.Hosting private IWebHost CreateHost(RequestDelegate requestDelegate) { var builder = CreateBuilder() - .UseServer(this) + .UseFakeServer() .Configure( appBuilder => { @@ -782,7 +844,7 @@ namespace Microsoft.AspNetCore.Hosting throw new InvalidOperationException(); }; - services.AddSingleton(new DelegateHostedService(started, stopping)); + services.AddSingleton(new DelegateHostedService(started, stopping, () => { })); return events; } @@ -802,35 +864,7 @@ namespace Microsoft.AspNetCore.Hosting return signals; } - public void Start(IHttpApplication application) - { - var startInstance = new StartInstance(); - _startInstances.Add(startInstance); - var context = application.CreateContext(Features); - try - { - application.ProcessRequestAsync(context); - } - catch (Exception ex) - { - application.DisposeContext(context, ex); - throw; - } - application.DisposeContext(context, null); - } - - public void Dispose() - { - if (_startInstances != null) - { - foreach (var startInstance in _startInstances) - { - startInstance.Dispose(); - } - } - } - - private class TestHostedService : IHostedService + private class TestHostedService : IHostedService, IDisposable { private readonly IApplicationLifetime _lifetime; @@ -841,6 +875,7 @@ namespace Microsoft.AspNetCore.Hosting public bool StartCalled { get; set; } public bool StopCalled { get; set; } + public bool DisposeCalled { get; set; } public void Start() { @@ -851,34 +886,117 @@ namespace Microsoft.AspNetCore.Hosting { StopCalled = true; } + + public void Dispose() + { + DisposeCalled = true; + } } - private class DelegateHostedService : IHostedService + private class DelegateHostedService : IHostedService, IDisposable { private readonly Action _started; private readonly Action _stopping; + private readonly Action _disposing; - public DelegateHostedService(Action started, Action stopping) + public DelegateHostedService(Action started, Action stopping, Action disposing) { _started = started; _stopping = stopping; + _disposing = disposing; } public void Start() => _started(); public void Stop() => _stopping(); + + public void Dispose() => _disposing(); } - private class StartInstance : IDisposable + public class StartInstance : IDisposable { + public int StopCalls { get; set; } + public int DisposeCalls { get; set; } + public void Stop() + { + StopCalls += 1; + } + public void Dispose() { DisposeCalls += 1; } } + public class FakeServer : IServer + { + public FakeServer() + { + Features = new FeatureCollection(); + Features.Set(new ServerAddressesFeature()); + } + + public IList StartInstances { get; } = new List(); + + public Func CreateRequestFeatures { get; set; } = NewFeatureCollection; + + public IFeatureCollection Features { get; } + + public static IFeatureCollection NewFeatureCollection() + { + var stub = new StubFeatures(); + var features = new FeatureCollection(); + features.Set(stub); + features.Set(stub); + return features; + } + + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) + { + var startInstance = new StartInstance(); + StartInstances.Add(startInstance); + var context = application.CreateContext(CreateRequestFeatures()); + try + { + application.ProcessRequestAsync(context); + } + catch (Exception ex) + { + application.DisposeContext(context, ex); + throw; + } + application.DisposeContext(context, null); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + if (StartInstances != null) + { + foreach (var startInstance in StartInstances) + { + startInstance.Stop(); + } + } + + return Task.CompletedTask; + } + + public void Dispose() + { + if (StartInstances != null) + { + foreach (var startInstance in StartInstances) + { + startInstance.Dispose(); + } + } + } + } + private class TestStartup : IStartup { public void Configure(IApplicationBuilder app) @@ -1040,4 +1158,12 @@ namespace Microsoft.AspNetCore.Hosting public string TraceIdentifier { get; set; } } } + + public static class TestServerWebHostExtensions + { + public static IWebHostBuilder UseFakeServer(this IWebHostBuilder builder) + { + return builder.ConfigureServices(services => services.AddSingleton()); + } + } } \ No newline at end of file