#947 Add IServer.StopAsyc, IWebHost.StopAsync, and make Start async

This commit is contained in:
Chris R 2017-03-27 10:30:45 -07:00
parent 5f9fa5c009
commit 62f74d5be0
18 changed files with 548 additions and 176 deletions

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -34,8 +36,9 @@ namespace SampleStartups
.Start(_urls.ToArray()); .Start(_urls.ToArray());
} }
public void Stop() public async Task StopAsync()
{ {
await _host.StopAsync(TimeSpan.FromSeconds(5));
_host.Dispose(); _host.Dispose();
} }

View File

@ -3,6 +3,8 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -166,7 +168,7 @@ namespace Microsoft.AspNetCore.Hosting
public static IWebHost Start(this IWebHostBuilder hostBuilder, params string[] urls) public static IWebHost Start(this IWebHostBuilder hostBuilder, params string[] urls)
{ {
var host = hostBuilder.UseUrls(urls).Build(); var host = hostBuilder.UseUrls(urls).Build();
host.Start(); host.StartAsync(CancellationToken.None).GetAwaiter().GetResult();
return host; return host;
} }
} }

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Hosting namespace Microsoft.AspNetCore.Hosting
@ -24,6 +26,13 @@ namespace Microsoft.AspNetCore.Hosting
/// <summary> /// <summary>
/// Starts listening on the configured addresses. /// Starts listening on the configured addresses.
/// </summary> /// </summary>
void Start(); Task StartAsync(CancellationToken cancellationToken);
/// <summary>
/// Attempt to gracefully stop the host.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task StopAsync(CancellationToken cancellationToken);
} }
} }

View File

@ -16,5 +16,7 @@ namespace Microsoft.AspNetCore.Hosting
public static readonly string ServerUrlsKey = "urls"; public static readonly string ServerUrlsKey = "urls";
public static readonly string ContentRootKey = "contentRoot"; public static readonly string ContentRootKey = "contentRoot";
public static readonly string PreferHostingUrls = "preferHostingUrls"; public static readonly string PreferHostingUrls = "preferHostingUrls";
public static readonly string ShutdownTimeoutKey = "shutdownTimeoutSeconds";
} }
} }

View File

@ -16,5 +16,24 @@
"NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHostBuilder", "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHostBuilder",
"NewMemberId": "Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureConfiguration(System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.Configuration.IConfigurationBuilder> configureDelegate)", "NewMemberId": "Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureConfiguration(System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.Configuration.IConfigurationBuilder> configureDelegate)",
"Kind": "Addition" "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"
} }
] ]

View File

@ -16,5 +16,24 @@
"NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHostBuilder", "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.IWebHostBuilder",
"NewMemberId": "Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureConfiguration(System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.Configuration.IConfigurationBuilder> configureDelegate)", "NewMemberId": "Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureConfiguration(System.Action<Microsoft.AspNetCore.Hosting.WebHostBuilderContext, Microsoft.Extensions.Configuration.IConfigurationBuilder> configureDelegate)",
"Kind": "Addition" "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"
} }
] ]

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Hosting.Server namespace Microsoft.AspNetCore.Hosting.Server
@ -21,6 +23,13 @@ namespace Microsoft.AspNetCore.Hosting.Server
/// </summary> /// </summary>
/// <param name="application">An instance of <see cref="IHttpApplication{TContext}"/>.</param> /// <param name="application">An instance of <see cref="IHttpApplication{TContext}"/>.</param>
/// <typeparam name="TContext">The context associated with the application.</typeparam> /// <typeparam name="TContext">The context associated with the application.</typeparam>
void Start<TContext>(IHttpApplication<TContext> application); /// <param name="cancellationToken">Indicates if the server startup should be aborted.</param>
Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken);
/// <summary>
/// Stop processing requests and shut down the server, gracefully if possible.
/// </summary>
/// <param name="cancellationToken">Indicates if the graceful shutdown should be aborted.</param>
Task StopAsync(CancellationToken cancellationToken);
} }
} }

View File

@ -10,5 +10,24 @@
"NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature", "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature",
"NewMemberId": "System.Void set_PreferHostingUrls(System.Boolean value)", "NewMemberId": "System.Void set_PreferHostingUrls(System.Boolean value)",
"Kind": "Addition" "Kind": "Addition"
},
{
"OldTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable",
"OldMemberId": "System.Void Start<T0>(Microsoft.AspNetCore.Hosting.Server.IHttpApplication<T0> application)",
"NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable",
"NewMemberId": "System.Threading.Tasks.Task StartAsync<T0>(Microsoft.AspNetCore.Hosting.Server.IHttpApplication<T0> 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<T0>(Microsoft.AspNetCore.Hosting.Server.IHttpApplication<T0> 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"
} }
] ]

View File

@ -10,5 +10,24 @@
"NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature", "NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature",
"NewMemberId": "System.Void set_PreferHostingUrls(System.Boolean value)", "NewMemberId": "System.Void set_PreferHostingUrls(System.Boolean value)",
"Kind": "Addition" "Kind": "Addition"
},
{
"OldTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable",
"OldMemberId": "System.Void Start<T0>(Microsoft.AspNetCore.Hosting.Server.IHttpApplication<T0> application)",
"NewTypeId": "public interface Microsoft.AspNetCore.Hosting.Server.IServer : System.IDisposable",
"NewMemberId": "System.Threading.Tasks.Task StartAsync<T0>(Microsoft.AspNetCore.Hosting.Server.IHttpApplication<T0> 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<T0>(Microsoft.AspNetCore.Hosting.Server.IHttpApplication<T0> 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"
} }
] ]

View File

@ -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<KeyValuePair<string, object>> private class HostingLogScope : IReadOnlyList<KeyValuePair<string, object>>
{ {

View File

@ -16,5 +16,6 @@ namespace Microsoft.AspNetCore.Hosting.Internal
public const int HostedServiceStartException = 9; public const int HostedServiceStartException = 9;
public const int HostedServiceStopException = 10; public const int HostedServiceStopException = 10;
public const int HostingStartupAssemblyException = 11; public const int HostingStartupAssemblyException = 11;
public const int ServerShutdownException = 12;
} }
} }

View File

@ -7,6 +7,8 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Builder; using Microsoft.AspNetCore.Hosting.Builder;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
@ -39,6 +41,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal
private RequestDelegate _application; private RequestDelegate _application;
private ILogger<WebHost> _logger; private ILogger<WebHost> _logger;
private bool _stopped;
// Used for testing only // Used for testing only
internal WebHostOptions Options => _options; 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(); HostingEventSource.Log.HostStart();
_logger = _applicationServices.GetRequiredService<ILogger<WebHost>>(); _logger = _applicationServices.GetRequiredService<ILogger<WebHost>>();
@ -112,7 +116,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal
_hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>(); _hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>();
var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>(); var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>();
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>(); var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
Server.Start(new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory)); var hostingApp = new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory);
await Server.StartAsync(hostingApp, cancellationToken);
// Fire IApplicationLifetime.Started // Fire IApplicationLifetime.Started
_applicationLifetime?.NotifyStarted(); _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(); _logger?.Shutdown();
if (!cancellationToken.CanBeCanceled)
{
cancellationToken = new CancellationTokenSource(Options.ShutdownTimeout).Token;
}
// Fire IApplicationLifetime.Stopping // Fire IApplicationLifetime.Stopping
_applicationLifetime?.StopApplication(); _applicationLifetime?.StopApplication();
await Server?.StopAsync(cancellationToken);
// Fire the IHostedService.Stop // Fire the IHostedService.Stop
_hostedServiceExecutor?.Stop(); _hostedServiceExecutor?.Stop();
(_hostingServiceProvider as IDisposable)?.Dispose();
(_applicationServices as IDisposable)?.Dispose();
// Fire IApplicationLifetime.Stopped // Fire IApplicationLifetime.Stopped
_applicationLifetime?.NotifyStopped(); _applicationLifetime?.NotifyStopped();
HostingEventSource.Log.HostStop(); 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();
}
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace Microsoft.AspNetCore.Hosting.Internal namespace Microsoft.AspNetCore.Hosting.Internal
@ -27,6 +28,13 @@ namespace Microsoft.AspNetCore.Hosting.Internal
ContentRootPath = configuration[WebHostDefaults.ContentRootKey]; ContentRootPath = configuration[WebHostDefaults.ContentRootKey];
HostingStartupAssemblies = configuration[WebHostDefaults.HostingStartupAssembliesKey]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0]; HostingStartupAssemblies = configuration[WebHostDefaults.HostingStartupAssembliesKey]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0];
PreferHostingUrls = ParseBool(configuration, WebHostDefaults.PreferHostingUrls); 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; } public string ApplicationName { get; set; }
@ -47,6 +55,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal
public bool PreferHostingUrls { get; set; } public bool PreferHostingUrls { get; set; }
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5);
private static bool ParseBool(IConfiguration configuration, string key) private static bool ParseBool(IConfiguration configuration, string key)
{ {
return string.Equals("true", configuration[key], StringComparison.OrdinalIgnoreCase) return string.Equals("true", configuration[key], StringComparison.OrdinalIgnoreCase)

View File

@ -7,6 +7,7 @@ using System.Reflection;
using System.Runtime.Loader; using System.Runtime.Loader;
#endif #endif
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -14,11 +15,62 @@ namespace Microsoft.AspNetCore.Hosting
{ {
public static class WebHostExtensions public static class WebHostExtensions
{ {
/// <summary>
/// Starts the host.
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
public static void Start(this IWebHost host)
{
host.StartAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Starts the host.
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
public static Task StartAsync(this IWebHost host)
{
return host.StartAsync(CancellationToken.None);
}
/// <summary>
/// Gracefully stops the host.
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
public static Task StopAsync(this IWebHost host)
{
return host.StopAsync(CancellationToken.None);
}
/// <summary>
/// Attempts to gracefully stop the host with the given timeout.
/// </summary>
/// <param name="host"></param>
/// <param name="timeout">The timeout for stopping gracefully. Once expired the
/// server may terminate any remaining active connections.</param>
/// <returns></returns>
public static Task StopAsync(this IWebHost host, TimeSpan timeout)
{
return host.StopAsync(new CancellationTokenSource(timeout).Token);
}
/// <summary> /// <summary>
/// Runs a web application and block the calling thread until host shutdown. /// Runs a web application and block the calling thread until host shutdown.
/// </summary> /// </summary>
/// <param name="host">The <see cref="IWebHost"/> to run.</param> /// <param name="host">The <see cref="IWebHost"/> to run.</param>
public static void Run(this IWebHost host) public static void Run(this IWebHost host)
{
host.RunAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Runs a web application and returns a Task that only completes on host shutdown.
/// </summary>
/// <param name="host">The <see cref="IWebHost"/> to run.</param>
public static async Task RunAsync(this IWebHost host)
{ {
var done = new ManualResetEventSlim(false); var done = new ManualResetEventSlim(false);
using (var cts = new CancellationTokenSource()) using (var cts = new CancellationTokenSource())
@ -28,7 +80,13 @@ namespace Microsoft.AspNetCore.Hosting
if (!cts.IsCancellationRequested) if (!cts.IsCancellationRequested)
{ {
Console.WriteLine("Application is shutting down..."); Console.WriteLine("Application is shutting down...");
cts.Cancel(); try
{
cts.Cancel();
}
catch (ObjectDisposedException)
{
}
} }
done.Wait(); done.Wait();
@ -48,26 +106,26 @@ namespace Microsoft.AspNetCore.Hosting
eventArgs.Cancel = true; 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(); done.Set();
} }
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="host">The <see cref="IWebHost"/> to run.</param> /// <param name="host">The <see cref="IWebHost"/> to run.</param>
/// <param name="token">The token to trigger shutdown.</param> /// <param name="token">The token to trigger shutdown.</param>
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) using (host)
{ {
host.Start(); await host.StartAsync(token);
var hostingEnvironment = host.Services.GetService<IHostingEnvironment>(); var hostingEnvironment = host.Services.GetService<IHostingEnvironment>();
var applicationLifetime = host.Services.GetService<IApplicationLifetime>(); var applicationLifetime = host.Services.GetService<IApplicationLifetime>();
@ -95,7 +153,17 @@ namespace Microsoft.AspNetCore.Hosting
}, },
applicationLifetime); applicationLifetime);
applicationLifetime.ApplicationStopping.WaitHandle.WaitOne(); var waitForStop = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
applicationLifetime.ApplicationStopping.Register(obj =>
{
var tcs = (TaskCompletionSource<object>)obj;
tcs.TrySetResult(null);
}, waitForStop);
await waitForStop.Task;
// WebHost will use its default ShutdownTimeout if none is specified.
await host.StopAsync();
} }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
@ -39,7 +40,7 @@ namespace Microsoft.AspNetCore.TestHost
Features = featureCollection; Features = featureCollection;
var host = builder.UseServer(this).Build(); var host = builder.UseServer(this).Build();
host.Start(); host.StartAsync().GetAwaiter().GetResult();
_hostInstance = host; _hostInstance = host;
} }
@ -91,7 +92,7 @@ namespace Microsoft.AspNetCore.TestHost
} }
} }
void IServer.Start<TContext>(IHttpApplication<TContext> application) Task IServer.StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
{ {
_application = new ApplicationWrapper<Context>((IHttpApplication<Context>)application, () => _application = new ApplicationWrapper<Context>((IHttpApplication<Context>)application, () =>
{ {
@ -100,6 +101,13 @@ namespace Microsoft.AspNetCore.TestHost
throw new ObjectDisposedException(GetType().FullName); throw new ObjectDisposedException(GetType().FullName);
} }
}); });
return Task.CompletedTask;
}
Task IServer.StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
} }
private class ApplicationWrapper<TContext> : IHttpApplication<TContext> private class ApplicationWrapper<TContext> : IHttpApplication<TContext>

View File

@ -1,11 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration;
namespace ServerComparison.TestSites namespace ServerComparison.TestSites
{ {
@ -36,8 +37,14 @@ namespace ServerComparison.TestSites
public IFeatureCollection Features { get; } = new FeatureCollection(); public IFeatureCollection Features { get; } = new FeatureCollection();
public void Start<TContext>(IHttpApplication<TContext> application) public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
{ {
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
} }
} }
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -48,7 +49,7 @@ namespace Microsoft.AspNetCore.Hosting
var host = builder.UseServer(server).UseStartup("MissingStartupAssembly").Build(); var host = builder.UseServer(server).UseStartup("MissingStartupAssembly").Build();
using (host) using (host)
{ {
host.Start(); await host.StartAsync();
await AssertResponseContains(server.RequestDelegate, "MissingStartupAssembly"); await AssertResponseContains(server.RequestDelegate, "MissingStartupAssembly");
} }
} }
@ -61,7 +62,7 @@ namespace Microsoft.AspNetCore.Hosting
var host = builder.UseServer(server).UseStartup<StartupStaticCtorThrows>().Build(); var host = builder.UseServer(server).UseStartup<StartupStaticCtorThrows>().Build();
using (host) using (host)
{ {
host.Start(); await host.StartAsync();
await AssertResponseContains(server.RequestDelegate, "Exception from static constructor"); await AssertResponseContains(server.RequestDelegate, "Exception from static constructor");
} }
} }
@ -74,7 +75,7 @@ namespace Microsoft.AspNetCore.Hosting
var host = builder.UseServer(server).UseStartup<StartupCtorThrows>().Build(); var host = builder.UseServer(server).UseStartup<StartupCtorThrows>().Build();
using (host) using (host)
{ {
host.Start(); await host.StartAsync();
await AssertResponseContains(server.RequestDelegate, "Exception from constructor"); await AssertResponseContains(server.RequestDelegate, "Exception from constructor");
} }
} }
@ -87,7 +88,7 @@ namespace Microsoft.AspNetCore.Hosting
var host = builder.UseServer(server).UseStartup<StartupThrowTypeLoadException>().Build(); var host = builder.UseServer(server).UseStartup<StartupThrowTypeLoadException>().Build();
using (host) using (host)
{ {
host.Start(); await host.StartAsync();
await AssertResponseContains(server.RequestDelegate, "Message from the LoaderException</div>"); await AssertResponseContains(server.RequestDelegate, "Message from the LoaderException</div>");
} }
} }
@ -100,7 +101,7 @@ namespace Microsoft.AspNetCore.Hosting
var host = builder.UseServer(server).UseStartup<StartupCtorThrows>().Build(); var host = builder.UseServer(server).UseStartup<StartupCtorThrows>().Build();
using (host) using (host)
{ {
host.Start(); await host.StartAsync();
var services = host.Services.GetServices<IApplicationLifetime>(); var services = host.Services.GetServices<IApplicationLifetime>();
Assert.NotNull(services); Assert.NotNull(services);
Assert.NotEmpty(services); Assert.NotEmpty(services);
@ -110,7 +111,7 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void DefaultObjectPoolProvider_IsRegistered() public async Task DefaultObjectPoolProvider_IsRegistered()
{ {
var server = new TestServer(); var server = new TestServer();
var host = CreateWebHostBuilder() var host = CreateWebHostBuilder()
@ -119,7 +120,7 @@ namespace Microsoft.AspNetCore.Hosting
.Build(); .Build();
using (host) using (host)
{ {
host.Start(); await host.StartAsync();
Assert.IsType<DefaultObjectPoolProvider>(host.Services.GetService<ObjectPoolProvider>()); Assert.IsType<DefaultObjectPoolProvider>(host.Services.GetService<ObjectPoolProvider>());
} }
} }
@ -132,7 +133,7 @@ namespace Microsoft.AspNetCore.Hosting
var host = builder.UseServer(server).UseStartup<StartupConfigureServicesThrows>().Build(); var host = builder.UseServer(server).UseStartup<StartupConfigureServicesThrows>().Build();
using (host) using (host)
{ {
host.Start(); await host.StartAsync();
await AssertResponseContains(server.RequestDelegate, "Exception from ConfigureServices"); await AssertResponseContains(server.RequestDelegate, "Exception from ConfigureServices");
} }
} }
@ -145,7 +146,7 @@ namespace Microsoft.AspNetCore.Hosting
var host = builder.UseServer(server).UseStartup<StartupConfigureServicesThrows>().Build(); var host = builder.UseServer(server).UseStartup<StartupConfigureServicesThrows>().Build();
using (host) using (host)
{ {
host.Start(); await host.StartAsync();
await AssertResponseContains(server.RequestDelegate, "Exception from Configure"); await AssertResponseContains(server.RequestDelegate, "Exception from Configure");
} }
} }
@ -810,7 +811,7 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void Build_DoesNotThrowIfUnloadableAssemblyNameInHostingStartupAssembliesAndCaptureStartupErrorsTrue() public async Task Build_DoesNotThrowIfUnloadableAssemblyNameInHostingStartupAssembliesAndCaptureStartupErrorsTrue()
{ {
var provider = new TestLoggerProvider(); var provider = new TestLoggerProvider();
var builder = CreateWebHostBuilder() var builder = CreateWebHostBuilder()
@ -825,7 +826,7 @@ namespace Microsoft.AspNetCore.Hosting
using (var host = builder.Build()) using (var host = builder.Build())
{ {
host.Start(); await host.StartAsync();
var context = provider.Sink.Writes.FirstOrDefault(s => s.EventId.Id == LoggerEventIds.HostingStartupAssemblyException); var context = provider.Sink.Writes.FirstOrDefault(s => s.EventId.Id == LoggerEventIds.HostingStartupAssemblyException);
Assert.NotNull(context); Assert.NotNull(context);
} }
@ -879,7 +880,7 @@ namespace Microsoft.AspNetCore.Hosting
} }
public void Start<TContext>(IHttpApplication<TContext> application) public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
{ {
RequestDelegate = async ctx => RequestDelegate = async ctx =>
{ {
@ -895,6 +896,13 @@ namespace Microsoft.AspNetCore.Hosting
} }
application.DisposeContext(httpContext, null); application.DisposeContext(httpContext, null);
}; };
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
} }
} }

View File

@ -23,40 +23,12 @@ using Xunit;
namespace Microsoft.AspNetCore.Hosting namespace Microsoft.AspNetCore.Hosting
{ {
public class WebHostTests : IServer public class WebHostTests
{ {
private readonly IList<StartInstance> _startInstances = new List<StartInstance>();
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<IHttpRequestFeature>(stub);
features.Set<IHttpResponseFeature>(stub);
features.Set<IServerAddressesFeature>(new ServerAddressesFeature());
return features;
}
[Fact] [Fact]
public void WebHostThrowsWithNoServer() public async Task WebHostThrowsWithNoServer()
{ {
var ex = Assert.Throws<InvalidOperationException>(() => CreateBuilder().Build().Start()); var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => CreateBuilder().Build().StartAsync());
Assert.Equal("No service for type 'Microsoft.AspNetCore.Hosting.Server.IServer' has been registered.", ex.Message); 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] [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<IServerAddressesFeature>(); var serverAddressesFeature = host.ServerFeatures.Get<IServerAddressesFeature>();
Assert.False(serverAddressesFeature.Addresses.Any()); Assert.False(serverAddressesFeature.Addresses.Any());
Assert.False(serverAddressesFeature.PreferHostingUrls); Assert.False(serverAddressesFeature.PreferHostingUrls);
@ -79,7 +51,7 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void UsesLegacyConfigurationForAddressesAndDoNotPreferHostingUrlsIfNotConfigured() public async Task UsesLegacyConfigurationForAddressesAndDoNotPreferHostingUrlsIfNotConfigured()
{ {
var data = new Dictionary<string, string> var data = new Dictionary<string, string>
{ {
@ -88,9 +60,9 @@ namespace Microsoft.AspNetCore.Hosting
var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); 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<IServerAddressesFeature>(); var serverAddressFeature = host.ServerFeatures.Get<IServerAddressesFeature>();
Assert.Equal("http://localhost:5002", serverAddressFeature.Addresses.First()); Assert.Equal("http://localhost:5002", serverAddressFeature.Addresses.First());
Assert.False(serverAddressFeature.PreferHostingUrls); Assert.False(serverAddressFeature.PreferHostingUrls);
@ -107,7 +79,7 @@ namespace Microsoft.AspNetCore.Hosting
var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build();
using (var host = CreateBuilder(config).UseServer(this).Build()) using (var host = CreateBuilder(config).UseFakeServer().Build())
{ {
host.Start(); host.Start();
var serverAddressFeature = host.ServerFeatures.Get<IServerAddressesFeature>(); var serverAddressFeature = host.ServerFeatures.Get<IServerAddressesFeature>();
@ -117,7 +89,7 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void UsesNewConfigurationOverLegacyConfigForAddressesAndDoNotPreferHostingUrlsIfNotConfigured() public async Task UsesNewConfigurationOverLegacyConfigForAddressesAndDoNotPreferHostingUrlsIfNotConfigured()
{ {
var data = new Dictionary<string, string> var data = new Dictionary<string, string>
{ {
@ -127,9 +99,9 @@ namespace Microsoft.AspNetCore.Hosting
var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); 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<IServerAddressesFeature>(); var serverAddressFeature = host.ServerFeatures.Get<IServerAddressesFeature>();
Assert.Equal("http://localhost:5009", serverAddressFeature.Addresses.First()); Assert.Equal("http://localhost:5009", serverAddressFeature.Addresses.First());
Assert.False(serverAddressFeature.PreferHostingUrls); Assert.False(serverAddressFeature.PreferHostingUrls);
@ -139,7 +111,7 @@ namespace Microsoft.AspNetCore.Hosting
[Fact] [Fact]
public void DoNotPreferHostingUrlsWhenNoAddressConfigured() public void DoNotPreferHostingUrlsWhenNoAddressConfigured()
{ {
using (var host = CreateBuilder().UseServer(this).PreferHostingUrls(true).Build()) using (var host = CreateBuilder().UseFakeServer().PreferHostingUrls(true).Build())
{ {
host.Start(); host.Start();
var serverAddressesFeature = host.ServerFeatures.Get<IServerAddressesFeature>(); var serverAddressesFeature = host.ServerFeatures.Get<IServerAddressesFeature>();
@ -149,7 +121,7 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void PreferHostingUrlsWhenAddressIsConfigured() public async Task PreferHostingUrlsWhenAddressIsConfigured()
{ {
var data = new Dictionary<string, string> var data = new Dictionary<string, string>
{ {
@ -158,9 +130,9 @@ namespace Microsoft.AspNetCore.Hosting
var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); 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<IServerAddressesFeature>().PreferHostingUrls); Assert.True(host.ServerFeatures.Get<IServerAddressesFeature>().PreferHostingUrls);
} }
} }
@ -169,17 +141,18 @@ namespace Microsoft.AspNetCore.Hosting
public void WebHostCanBeStarted() public void WebHostCanBeStarted()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.UseStartup("Microsoft.AspNetCore.Hosting.Tests") .UseStartup("Microsoft.AspNetCore.Hosting.Tests")
.Start()) .Start())
{ {
var server = (FakeServer)host.Services.GetRequiredService<IServer>();
Assert.NotNull(host); Assert.NotNull(host);
Assert.Equal(1, _startInstances.Count); Assert.Equal(1, server.StartInstances.Count);
Assert.Equal(0, _startInstances[0].DisposeCalls); Assert.Equal(0, server.StartInstances[0].DisposeCalls);
host.Dispose(); 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() public void WebHostShutsDownWhenTokenTriggers()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.UseStartup("Microsoft.AspNetCore.Hosting.Tests") .UseStartup("Microsoft.AspNetCore.Hosting.Tests")
.Build()) .Build())
{ {
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
var server = (FakeServer)host.Services.GetRequiredService<IServer>();
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
Task.Run(() => host.Run(cts.Token)); var runInBackground = host.RunAsync(cts.Token);
// Wait on the host to be started // Wait on the host to be started
lifetime.ApplicationStarted.WaitHandle.WaitOne(); lifetime.ApplicationStarted.WaitHandle.WaitOne();
Assert.Equal(1, _startInstances.Count); Assert.Equal(1, server.StartInstances.Count);
Assert.Equal(0, _startInstances[0].DisposeCalls); Assert.Equal(0, server.StartInstances[0].DisposeCalls);
cts.Cancel(); cts.Cancel();
// Wait on the host to shutdown // Wait on the host to shutdown
lifetime.ApplicationStopped.WaitHandle.WaitOne(); 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() public void WebHostApplicationLifetimeEventsOrderedCorrectlyDuringShutdown()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.UseStartup("Microsoft.AspNetCore.Hosting.Tests") .UseStartup("Microsoft.AspNetCore.Hosting.Tests")
.Build()) .Build())
{ {
@ -251,9 +225,9 @@ namespace Microsoft.AspNetCore.Hosting
applicationStoppedEvent.Set(); applicationStoppedEvent.Set();
}); });
var runHostAndVerifyApplicationStopped = Task.Run(() => var runHostAndVerifyApplicationStopped = Task.Run(async () =>
{ {
host.Run(); await host.RunAsync();
// Check whether the applicationStoppingEvent has been set // Check whether the applicationStoppingEvent has been set
applicationStoppedCompletedBeforeRunCompleted = applicationStoppedEvent.IsSet; applicationStoppedCompletedBeforeRunCompleted = applicationStoppedEvent.IsSet;
}); });
@ -275,10 +249,10 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void WebHostDisposesServiceProvider() public async Task WebHostDisposesServiceProvider()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.ConfigureServices(s => .ConfigureServices(s =>
{ {
s.AddTransient<IFakeService, FakeService>(); s.AddTransient<IFakeService, FakeService>();
@ -287,7 +261,7 @@ namespace Microsoft.AspNetCore.Hosting
.UseStartup("Microsoft.AspNetCore.Hosting.Tests") .UseStartup("Microsoft.AspNetCore.Hosting.Tests")
.Build()) .Build())
{ {
host.Start(); await host.StartAsync();
var singleton = (FakeService)host.Services.GetService<IFakeSingletonService>(); var singleton = (FakeService)host.Services.GetService<IFakeSingletonService>();
var transient = (FakeService)host.Services.GetService<IFakeService>(); var transient = (FakeService)host.Services.GetService<IFakeService>();
@ -295,6 +269,11 @@ namespace Microsoft.AspNetCore.Hosting
Assert.False(singleton.Disposed); Assert.False(singleton.Disposed);
Assert.False(transient.Disposed); Assert.False(transient.Disposed);
await host.StopAsync();
Assert.False(singleton.Disposed);
Assert.False(transient.Disposed);
host.Dispose(); host.Dispose();
Assert.True(singleton.Disposed); Assert.True(singleton.Disposed);
@ -303,26 +282,26 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void WebHostNotifiesApplicationStarted() public async Task WebHostNotifiesApplicationStarted()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.Build()) .Build())
{ {
var applicationLifetime = host.Services.GetService<IApplicationLifetime>(); var applicationLifetime = host.Services.GetService<IApplicationLifetime>();
Assert.False(applicationLifetime.ApplicationStarted.IsCancellationRequested); Assert.False(applicationLifetime.ApplicationStarted.IsCancellationRequested);
host.Start(); await host.StartAsync();
Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested); Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested);
} }
} }
[Fact] [Fact]
public void WebHostNotifiesAllIApplicationLifetimeCallbacksEvenIfTheyThrow() public async Task WebHostNotifiesAllIApplicationLifetimeCallbacksEvenIfTheyThrow()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.Build()) .Build())
{ {
var applicationLifetime = host.Services.GetService<IApplicationLifetime>(); var applicationLifetime = host.Services.GetService<IApplicationLifetime>();
@ -331,7 +310,7 @@ namespace Microsoft.AspNetCore.Hosting
var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
var stopped = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopped); var stopped = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopped);
host.Start(); await host.StartAsync();
Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested); Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested);
Assert.True(started.All(s => s)); Assert.True(started.All(s => s));
host.Dispose(); host.Dispose();
@ -341,13 +320,13 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void WebHostNotifiesAllIApplicationLifetimeEventsCallbacksEvenIfTheyThrow() public async Task WebHostNotifiesAllIApplicationLifetimeEventsCallbacksEvenIfTheyThrow()
{ {
bool[] events1 = null; bool[] events1 = null;
bool[] events2 = null; bool[] events2 = null;
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
events1 = RegisterCallbacksThatThrow(services); events1 = RegisterCallbacksThatThrow(services);
@ -355,7 +334,7 @@ namespace Microsoft.AspNetCore.Hosting
}) })
.Build()) .Build())
{ {
host.Start(); await host.StartAsync();
Assert.True(events1[0]); Assert.True(events1[0]);
Assert.True(events2[0]); Assert.True(events2[0]);
host.Dispose(); host.Dispose();
@ -365,12 +344,13 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void WebHostStopApplicationDoesNotFireStopOnHostedService() public async Task WebHostStopApplicationDoesNotFireStopOnHostedService()
{ {
var stoppingCalls = 0; var stoppingCalls = 0;
var disposingCalls = 0;
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
Action started = () => Action started = () =>
@ -382,24 +362,32 @@ namespace Microsoft.AspNetCore.Hosting
stoppingCalls++; stoppingCalls++;
}; };
services.AddSingleton<IHostedService>(new DelegateHostedService(started, stopping)); Action disposing = () =>
{
disposingCalls++;
};
services.AddSingleton<IHostedService>(_ => new DelegateHostedService(started, stopping, disposing));
}) })
.Build()) .Build())
{ {
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
lifetime.StopApplication(); lifetime.StopApplication();
host.Start(); await host.StartAsync();
Assert.Equal(0, stoppingCalls); Assert.Equal(0, stoppingCalls);
Assert.Equal(0, disposingCalls);
} }
Assert.Equal(1, stoppingCalls);
Assert.Equal(1, disposingCalls);
} }
[Fact] [Fact]
public void HostedServiceCanInjectApplicationLifetime() public async Task HostedServiceCanInjectApplicationLifetime()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddSingleton<IHostedService, TestHostedService>(); services.AddSingleton<IHostedService, TestHostedService>();
@ -409,19 +397,21 @@ namespace Microsoft.AspNetCore.Hosting
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
lifetime.StopApplication(); lifetime.StopApplication();
host.Start(); await host.StartAsync();
var svc = (TestHostedService)host.Services.GetRequiredService<IHostedService>(); var svc = (TestHostedService)host.Services.GetRequiredService<IHostedService>();
Assert.True(svc.StartCalled); Assert.True(svc.StartCalled);
host.Dispose();
await host.StopAsync();
Assert.True(svc.StopCalled); Assert.True(svc.StopCalled);
host.Dispose();
} }
} }
[Fact] [Fact]
public void HostedServiceStartNotCalledIfWebHostNotStarted() public async Task HostedServiceStartNotCalledIfWebHostNotStarted()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddSingleton<IHostedService, TestHostedService>(); services.AddSingleton<IHostedService, TestHostedService>();
@ -433,19 +423,23 @@ namespace Microsoft.AspNetCore.Hosting
var svc = (TestHostedService)host.Services.GetRequiredService<IHostedService>(); var svc = (TestHostedService)host.Services.GetRequiredService<IHostedService>();
Assert.False(svc.StartCalled); Assert.False(svc.StartCalled);
await host.StopAsync();
Assert.False(svc.StopCalled);
host.Dispose(); host.Dispose();
Assert.False(svc.StopCalled); Assert.False(svc.StopCalled);
Assert.True(svc.DisposeCalled);
} }
} }
[Fact] [Fact]
public void WebHostDisposeApplicationFiresStopOnHostedService() public async Task WebHostStopApplicationFiresStopOnHostedService()
{ {
var stoppingCalls = 0; var stoppingCalls = 0;
var startedCalls = 0; var startedCalls = 0;
var disposingCalls = 0;
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
Action started = () => Action started = () =>
@ -458,28 +452,90 @@ namespace Microsoft.AspNetCore.Hosting
stoppingCalls++; stoppingCalls++;
}; };
services.AddSingleton<IHostedService>(new DelegateHostedService(started, stopping)); Action disposing = () =>
{
disposingCalls++;
};
services.AddSingleton<IHostedService>(_ => new DelegateHostedService(started, stopping, disposing));
}) })
.Build()) .Build())
{ {
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>(); var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
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(); host.Dispose();
Assert.Equal(1, startedCalls); Assert.Equal(1, startedCalls);
Assert.Equal(1, stoppingCalls); Assert.Equal(1, stoppingCalls);
Assert.Equal(1, disposingCalls);
} }
} }
[Fact] [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<IHostedService>(_ => new DelegateHostedService(started, stopping, disposing));
})
.Build())
{
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
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[] events1 = null;
bool[] events2 = null; bool[] events2 = null;
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
events1 = RegisterCallbacksThatThrow(services); events1 = RegisterCallbacksThatThrow(services);
@ -492,7 +548,7 @@ namespace Microsoft.AspNetCore.Hosting
var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted); var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted);
var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
host.Start(); await host.StartAsync();
Assert.True(events1[0]); Assert.True(events1[0]);
Assert.True(events2[0]); Assert.True(events2[0]);
Assert.True(started.All(s => s)); Assert.True(started.All(s => s));
@ -504,15 +560,15 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void WebHostInjectsHostingEnvironment() public async Task WebHostInjectsHostingEnvironment()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.UseStartup("Microsoft.AspNetCore.Hosting.Tests") .UseStartup("Microsoft.AspNetCore.Hosting.Tests")
.UseEnvironment("WithHostingEnvironment") .UseEnvironment("WithHostingEnvironment")
.Build()) .Build())
{ {
host.Start(); await host.StartAsync();
var env = host.Services.GetService<IHostingEnvironment>(); var env = host.Services.GetService<IHostingEnvironment>();
Assert.Equal("Changed", env.EnvironmentName); Assert.Equal("Changed", env.EnvironmentName);
} }
@ -526,7 +582,7 @@ namespace Microsoft.AspNetCore.Hosting
{ {
services.AddTransient<IStartup, TestStartup>(); services.AddTransient<IStartup, TestStartup>();
}) })
.UseServer(this) .UseFakeServer()
.UseStartup("Microsoft.AspNetCore.Hosting.Tests"); .UseStartup("Microsoft.AspNetCore.Hosting.Tests");
Assert.Throws<NotImplementedException>(() => builder.Build()); Assert.Throws<NotImplementedException>(() => builder.Build());
@ -535,7 +591,7 @@ namespace Microsoft.AspNetCore.Hosting
[Fact] [Fact]
public void CanCreateApplicationServicesWithAddedServices() 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<IOptions<object>>()); Assert.NotNull(host.Services.GetRequiredService<IOptions<object>>());
} }
@ -547,7 +603,7 @@ namespace Microsoft.AspNetCore.Hosting
// Verify ordering // Verify ordering
var configureOrder = 0; var configureOrder = 0;
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddTransient<IStartupFilter>(serviceProvider => new TestFilter( services.AddTransient<IStartupFilter>(serviceProvider => new TestFilter(
@ -593,7 +649,7 @@ namespace Microsoft.AspNetCore.Hosting
[Fact] [Fact]
public void EnvDefaultsToProductionIfNoConfig() public void EnvDefaultsToProductionIfNoConfig()
{ {
using (var host = CreateBuilder().UseServer(this).Build()) using (var host = CreateBuilder().UseFakeServer().Build())
{ {
var env = host.Services.GetService<IHostingEnvironment>(); var env = host.Services.GetService<IHostingEnvironment>();
Assert.Equal(EnvironmentName.Production, env.EnvironmentName); Assert.Equal(EnvironmentName.Production, env.EnvironmentName);
@ -612,7 +668,7 @@ namespace Microsoft.AspNetCore.Hosting
.AddInMemoryCollection(vals); .AddInMemoryCollection(vals);
var config = builder.Build(); var config = builder.Build();
using (var host = CreateBuilder(config).UseServer(this).Build()) using (var host = CreateBuilder(config).UseFakeServer().Build())
{ {
var env = host.Services.GetService<IHostingEnvironment>(); var env = host.Services.GetService<IHostingEnvironment>();
Assert.Equal("Staging", env.EnvironmentName); Assert.Equal("Staging", env.EnvironmentName);
@ -631,7 +687,7 @@ namespace Microsoft.AspNetCore.Hosting
.AddInMemoryCollection(vals); .AddInMemoryCollection(vals);
var config = builder.Build(); var config = builder.Build();
using (var host = CreateBuilder(config).UseServer(this).Build()) using (var host = CreateBuilder(config).UseFakeServer().Build())
{ {
var env = host.Services.GetService<IHostingEnvironment>(); var env = host.Services.GetService<IHostingEnvironment>();
Assert.Equal(Path.GetFullPath("testroot"), env.WebRootPath); Assert.Equal(Path.GetFullPath("testroot"), env.WebRootPath);
@ -640,11 +696,11 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [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<IHostingEnvironment>(); var env = host.Services.GetRequiredService<IHostingEnvironment>();
Assert.True(env.IsEnvironment(EnvironmentName.Production)); Assert.True(env.IsEnvironment(EnvironmentName.Production));
Assert.True(env.IsEnvironment("producTion")); Assert.True(env.IsEnvironment("producTion"));
@ -652,7 +708,7 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void WebHost_CreatesDefaultRequestIdentifierFeature_IfNotPresent() public async Task WebHost_CreatesDefaultRequestIdentifierFeature_IfNotPresent()
{ {
// Arrange // Arrange
HttpContext httpContext = null; HttpContext httpContext = null;
@ -665,7 +721,7 @@ namespace Microsoft.AspNetCore.Hosting
using (var host = CreateHost(requestDelegate)) using (var host = CreateHost(requestDelegate))
{ {
// Act // Act
host.Start(); await host.StartAsync();
// Assert // Assert
Assert.NotNull(httpContext); Assert.NotNull(httpContext);
@ -676,7 +732,7 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void WebHost_DoesNot_CreateDefaultRequestIdentifierFeature_IfPresent() public async Task WebHost_DoesNot_CreateDefaultRequestIdentifierFeature_IfPresent()
{ {
// Arrange // Arrange
HttpContext httpContext = null; HttpContext httpContext = null;
@ -686,12 +742,18 @@ namespace Microsoft.AspNetCore.Hosting
return Task.FromResult(0); return Task.FromResult(0);
}); });
var requestIdentifierFeature = new StubHttpRequestIdentifierFeature(); var requestIdentifierFeature = new StubHttpRequestIdentifierFeature();
_featuresSupportedByThisHost[typeof(IHttpRequestIdentifierFeature)] = requestIdentifierFeature;
using (var host = CreateHost(requestDelegate)) using (var host = CreateHost(requestDelegate))
{ {
var server = (FakeServer)host.Services.GetRequiredService<IServer>();
server.CreateRequestFeatures = () =>
{
var features = FakeServer.NewFeatureCollection();
features.Set<IHttpRequestIdentifierFeature>(requestIdentifierFeature);
return features;
};
// Act // Act
host.Start(); await host.StartAsync();
// Assert // Assert
Assert.NotNull(httpContext); Assert.NotNull(httpContext);
@ -700,14 +762,14 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public void WebHost_InvokesConfigureMethodsOnlyOnce() public async Task WebHost_InvokesConfigureMethodsOnlyOnce()
{ {
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseServer(this) .UseFakeServer()
.UseStartup<CountStartup>() .UseStartup<CountStartup>()
.Build()) .Build())
{ {
host.Start(); await host.StartAsync();
var services = host.Services; var services = host.Services;
var services2 = host.Services; var services2 = host.Services;
Assert.Equal(1, CountStartup.ConfigureCount); Assert.Equal(1, CountStartup.ConfigureCount);
@ -735,7 +797,7 @@ namespace Microsoft.AspNetCore.Hosting
public void WebHost_ThrowsForBadConfigureServiceSignature() public void WebHost_ThrowsForBadConfigureServiceSignature()
{ {
var builder = CreateBuilder() var builder = CreateBuilder()
.UseServer(this) .UseFakeServer()
.UseStartup<BadConfigureServicesStartup>(); .UseStartup<BadConfigureServicesStartup>();
var ex = Assert.Throws<InvalidOperationException>(() => builder.Build()); var ex = Assert.Throws<InvalidOperationException>(() => builder.Build());
@ -751,7 +813,7 @@ namespace Microsoft.AspNetCore.Hosting
private IWebHost CreateHost(RequestDelegate requestDelegate) private IWebHost CreateHost(RequestDelegate requestDelegate)
{ {
var builder = CreateBuilder() var builder = CreateBuilder()
.UseServer(this) .UseFakeServer()
.Configure( .Configure(
appBuilder => appBuilder =>
{ {
@ -782,7 +844,7 @@ namespace Microsoft.AspNetCore.Hosting
throw new InvalidOperationException(); throw new InvalidOperationException();
}; };
services.AddSingleton<IHostedService>(new DelegateHostedService(started, stopping)); services.AddSingleton<IHostedService>(new DelegateHostedService(started, stopping, () => { }));
return events; return events;
} }
@ -802,35 +864,7 @@ namespace Microsoft.AspNetCore.Hosting
return signals; return signals;
} }
public void Start<TContext>(IHttpApplication<TContext> application) private class TestHostedService : IHostedService, IDisposable
{
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 readonly IApplicationLifetime _lifetime; private readonly IApplicationLifetime _lifetime;
@ -841,6 +875,7 @@ namespace Microsoft.AspNetCore.Hosting
public bool StartCalled { get; set; } public bool StartCalled { get; set; }
public bool StopCalled { get; set; } public bool StopCalled { get; set; }
public bool DisposeCalled { get; set; }
public void Start() public void Start()
{ {
@ -851,34 +886,117 @@ namespace Microsoft.AspNetCore.Hosting
{ {
StopCalled = true; StopCalled = true;
} }
public void Dispose()
{
DisposeCalled = true;
}
} }
private class DelegateHostedService : IHostedService private class DelegateHostedService : IHostedService, IDisposable
{ {
private readonly Action _started; private readonly Action _started;
private readonly Action _stopping; private readonly Action _stopping;
private readonly Action _disposing;
public DelegateHostedService(Action started, Action stopping) public DelegateHostedService(Action started, Action stopping, Action disposing)
{ {
_started = started; _started = started;
_stopping = stopping; _stopping = stopping;
_disposing = disposing;
} }
public void Start() => _started(); public void Start() => _started();
public void Stop() => _stopping(); 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 int DisposeCalls { get; set; }
public void Stop()
{
StopCalls += 1;
}
public void Dispose() public void Dispose()
{ {
DisposeCalls += 1; DisposeCalls += 1;
} }
} }
public class FakeServer : IServer
{
public FakeServer()
{
Features = new FeatureCollection();
Features.Set<IServerAddressesFeature>(new ServerAddressesFeature());
}
public IList<StartInstance> StartInstances { get; } = new List<StartInstance>();
public Func<IFeatureCollection> CreateRequestFeatures { get; set; } = NewFeatureCollection;
public IFeatureCollection Features { get; }
public static IFeatureCollection NewFeatureCollection()
{
var stub = new StubFeatures();
var features = new FeatureCollection();
features.Set<IHttpRequestFeature>(stub);
features.Set<IHttpResponseFeature>(stub);
return features;
}
public Task StartAsync<TContext>(IHttpApplication<TContext> 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 private class TestStartup : IStartup
{ {
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)
@ -1040,4 +1158,12 @@ namespace Microsoft.AspNetCore.Hosting
public string TraceIdentifier { get; set; } public string TraceIdentifier { get; set; }
} }
} }
public static class TestServerWebHostExtensions
{
public static IWebHostBuilder UseFakeServer(this IWebHostBuilder builder)
{
return builder.ConfigureServices(services => services.AddSingleton<IServer, WebHostTests.FakeServer>());
}
}
} }