From cfe9b26a34af15c4943240d790ee5d1a168290c9 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Tue, 13 Nov 2018 21:22:30 -0800 Subject: [PATCH] Added support for generic host based IWebHostBuilder (#1580) - This adds an implementation of IWebHostBuilder as a facade over the IHostBuilder. This removes the 2 container issue by executing the Startup.ConfigureServies and Startup.ConfigureContainer inline as part of building the IHostBuilder. - The implementation is highly compatible implementation since it exposes the same IWebHostBuilder interface. Existing extensions mostly work. - There are some caveats with this approach. - Injecting services into Startup is not extremely constrained to the services availble on HostBuilderContext. This includes the IHostingEnvironment and the IConfiguration. - IStartup is broken when using this pattern because it isn't composable. - The IStartupConfigureServicesFilter and IStartupConfigureContainer The before and after filters added in 2.1 are also broken because there's a single container (it could maybe be fixed by downcasting and doing something specific on the GenericHostBuilder instance). - Calling into IWebHostBuilder.Build will throw a NotSupportedException since this implementation is just a facade over the IHostBuilder. --- samples/GenericWebHost/Program.cs | 19 +- samples/GenericWebHost/WebHostExtensions.cs | 43 -- samples/GenericWebHost/WebHostService.cs | 62 -- .../GenericWebHost/WebHostServiceOptions.cs | 11 - .../GenericWebHostApplicationLifetime.cs | 24 + .../GenericHost/GenericWebHostBuilder.cs | 376 ++++++++++++ .../GenericWebHostServiceOptions.cs | 17 + .../GenericHost/GenericWebHostedService.cs | 201 +++++++ .../HostingStartupWebHostBuilder.cs | 79 +++ .../GenericHost/ISupportsStartup.cs | 14 + .../ISupportsUseDefaultServiceProvider.cs | 13 + .../GenericHostWebHostBuilderExtensions.cs | 16 + .../Internal/ConfigureContainerBuilder.cs | 2 +- .../Internal/ConfigureServicesBuilder.cs | 2 +- .../Internal/StartupLoader.cs | 6 +- .../Internal/WebHost.cs | 2 +- .../WebHostBuilderExtensions.cs | 35 +- .../Fakes/GenericWebHost.cs | 35 ++ .../Fakes/GenericWebHostBuilderWrapper.cs | 77 +++ .../Fakes/StartupNoServicesNoInterface.cs | 21 + .../Microsoft.AspNetCore.Hosting.Tests.csproj | 1 + .../WebHostBuilderTests.cs | 534 +++++++++++------- 22 files changed, 1250 insertions(+), 340 deletions(-) delete mode 100644 samples/GenericWebHost/WebHostExtensions.cs delete mode 100644 samples/GenericWebHost/WebHostService.cs delete mode 100644 samples/GenericWebHost/WebHostServiceOptions.cs create mode 100644 src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostApplicationLifetime.cs create mode 100644 src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostServiceOptions.cs create mode 100644 src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostedService.cs create mode 100644 src/Microsoft.AspNetCore.Hosting/GenericHost/HostingStartupWebHostBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Hosting/GenericHost/ISupportsStartup.cs create mode 100644 src/Microsoft.AspNetCore.Hosting/GenericHost/ISupportsUseDefaultServiceProvider.cs create mode 100644 src/Microsoft.AspNetCore.Hosting/GenericHostWebHostBuilderExtensions.cs create mode 100644 test/Microsoft.AspNetCore.Hosting.Tests/Fakes/GenericWebHost.cs create mode 100644 test/Microsoft.AspNetCore.Hosting.Tests/Fakes/GenericWebHostBuilderWrapper.cs create mode 100644 test/Microsoft.AspNetCore.Hosting.Tests/Fakes/StartupNoServicesNoInterface.cs diff --git a/samples/GenericWebHost/Program.cs b/samples/GenericWebHost/Program.cs index 4879031f..653541ef 100644 --- a/samples/GenericWebHost/Program.cs +++ b/samples/GenericWebHost/Program.cs @@ -1,10 +1,9 @@ -using System; -using System.Net; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Hosting; namespace GenericWebHost { @@ -19,22 +18,20 @@ namespace GenericWebHost config.AddJsonFile("appsettings.json", optional: true); config.AddCommandLine(args); }) - .ConfigureServices((hostContext, services) => - { - }) .UseFakeServer() - .ConfigureWebHost((hostContext, app) => + .ConfigureWebHost(builder => { - app.Run(async (context) => + builder.Configure(app => { - await context.Response.WriteAsync("Hello World!"); + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); }); }) .UseConsoleLifetime() .Build(); - var s = host.Services; - await host.RunAsync(); } } diff --git a/samples/GenericWebHost/WebHostExtensions.cs b/samples/GenericWebHost/WebHostExtensions.cs deleted file mode 100644 index bf5567d8..00000000 --- a/samples/GenericWebHost/WebHostExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Hosting.Internal; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ObjectPool; - -namespace GenericWebHost -{ - public static class WebHostExtensions - { - public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action configureApp) - { - return builder.ConfigureServices((bulderContext, services) => - { - services.Configure(options => - { - options.ConfigureApp = configureApp; - }); - services.AddHostedService(); - - var listener = new DiagnosticListener("Microsoft.AspNetCore"); - services.AddSingleton(listener); - services.AddSingleton(listener); - - services.AddTransient(); - services.AddScoped(); - - // Conjure up a RequestServices - services.AddTransient(); - services.AddTransient, DefaultServiceProviderFactory>(); - - // Ensure object pooling is available everywhere. - services.AddSingleton(); - }); - } - } -} diff --git a/samples/GenericWebHost/WebHostService.cs b/samples/GenericWebHost/WebHostService.cs deleted file mode 100644 index 1ac31617..00000000 --- a/samples/GenericWebHost/WebHostService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder.Internal; -using Microsoft.AspNetCore.Hosting.Internal; -using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Hosting.Server.Features; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace GenericWebHost -{ - internal class WebHostService : IHostedService - { - public WebHostService(IOptions options, IServiceProvider services, HostBuilderContext hostBuilderContext, IServer server, - ILogger logger, DiagnosticListener diagnosticListener, IHttpContextFactory httpContextFactory) - { - Options = options?.Value ?? throw new System.ArgumentNullException(nameof(options)); - - if (Options.ConfigureApp == null) - { - throw new ArgumentException(nameof(Options.ConfigureApp)); - } - - Services = services ?? throw new ArgumentNullException(nameof(services)); - HostBuilderContext = hostBuilderContext ?? throw new ArgumentNullException(nameof(hostBuilderContext)); - Server = server ?? throw new ArgumentNullException(nameof(server)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - DiagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener)); - HttpContextFactory = httpContextFactory ?? throw new ArgumentNullException(nameof(httpContextFactory)); - } - - public WebHostServiceOptions Options { get; } - public IServiceProvider Services { get; } - public HostBuilderContext HostBuilderContext { get; } - public IServer Server { get; } - public ILogger Logger { get; } - public DiagnosticListener DiagnosticListener { get; } - public IHttpContextFactory HttpContextFactory { get; } - - public Task StartAsync(CancellationToken cancellationToken) - { - Server.Features.Get()?.Addresses.Add("http://localhost:5000"); - - var builder = new ApplicationBuilder(Services, Server.Features); - Options.ConfigureApp(HostBuilderContext, builder); - var app = builder.Build(); - - var httpApp = new HostingApplication(app, Logger, DiagnosticListener, HttpContextFactory); - return Server.StartAsync(httpApp, cancellationToken); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Server.StopAsync(cancellationToken); - } - } -} \ No newline at end of file diff --git a/samples/GenericWebHost/WebHostServiceOptions.cs b/samples/GenericWebHost/WebHostServiceOptions.cs deleted file mode 100644 index 123dcf87..00000000 --- a/samples/GenericWebHost/WebHostServiceOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Hosting; - -namespace GenericWebHost -{ - public class WebHostServiceOptions - { - public Action ConfigureApp { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostApplicationLifetime.cs b/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostApplicationLifetime.cs new file mode 100644 index 00000000..d9570415 --- /dev/null +++ b/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostApplicationLifetime.cs @@ -0,0 +1,24 @@ +// 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 System.Threading; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal class GenericWebHostApplicationLifetime : IApplicationLifetime + { + private readonly Microsoft.Extensions.Hosting.IApplicationLifetime _applicationLifetime; + public GenericWebHostApplicationLifetime(Microsoft.Extensions.Hosting.IApplicationLifetime applicationLifetime) + { + _applicationLifetime = applicationLifetime; + } + + public CancellationToken ApplicationStarted => _applicationLifetime.ApplicationStarted; + + public CancellationToken ApplicationStopping => _applicationLifetime.ApplicationStopping; + + public CancellationToken ApplicationStopped => _applicationLifetime.ApplicationStopped; + + public void StopApplication() => _applicationLifetime.StopApplication(); + } +} diff --git a/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostBuilder.cs b/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostBuilder.cs new file mode 100644 index 00000000..6a425883 --- /dev/null +++ b/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostBuilder.cs @@ -0,0 +1,376 @@ +// 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 System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider + { + private readonly IHostBuilder _builder; + private readonly IConfiguration _config; + private readonly object _startupKey = new object(); + + private AggregateException _hostingStartupErrors; + private HostingStartupWebHostBuilder _hostingStartupWebHostBuilder; + + public GenericWebHostBuilder(IHostBuilder builder) + { + _builder = builder; + + _config = new ConfigurationBuilder() + .AddEnvironmentVariables(prefix: "ASPNETCORE_") + .Build(); + + _builder.ConfigureHostConfiguration(config => + { + config.AddConfiguration(_config); + + // We do this super early but still late enough that we can process the configuration + // wired up by calls to UseSetting + ExecuteHostingStartups(); + }); + + // IHostingStartup needs to be executed before any direct methods on the builder + // so register these callbacks first + _builder.ConfigureAppConfiguration((context, configurationBuilder) => + { + if (_hostingStartupWebHostBuilder != null) + { + var webhostContext = GetWebHostBuilderContext(context); + _hostingStartupWebHostBuilder.ConfigureAppConfiguration(webhostContext, configurationBuilder); + } + }); + + _builder.ConfigureServices((context, services) => + { + if (_hostingStartupWebHostBuilder != null) + { + var webhostContext = GetWebHostBuilderContext(context); + _hostingStartupWebHostBuilder.ConfigureServices(webhostContext, services); + } + }); + + _builder.ConfigureServices((context, services) => + { + var webhostContext = GetWebHostBuilderContext(context); + var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)]; + + // Add the IHostingEnvironment and IApplicationLifetime from Microsoft.AspNetCore.Hosting + services.AddSingleton(webhostContext.HostingEnvironment); + services.AddSingleton(); + + services.Configure(options => + { + // Set the options + options.WebHostOptions = webHostOptions; + // Store and forward any startup errors + options.HostingStartupExceptions = _hostingStartupErrors; + }); + + services.AddHostedService(); + + // REVIEW: This is bad since we don't own this type. Anybody could add one of these and it would mess things up + // We need to flow this differently + var listener = new DiagnosticListener("Microsoft.AspNetCore"); + services.TryAddSingleton(listener); + services.TryAddSingleton(listener); + + services.TryAddSingleton(); + services.TryAddScoped(); + services.TryAddSingleton(); + + // Conjure up a RequestServices + services.TryAddTransient(); + + // Ensure object pooling is available everywhere. + services.TryAddSingleton(); + + // Support UseStartup(assemblyName) + if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly)) + { + try + { + var startupType = StartupLoader.FindStartupType(webHostOptions.StartupAssembly, webhostContext.HostingEnvironment.EnvironmentName); + UseStartup(startupType, context, services); + } + catch (Exception ex) when (webHostOptions.CaptureStartupErrors) + { + var capture = ExceptionDispatchInfo.Capture(ex); + + services.Configure(options => + { + options.ConfigureApplication = app => + { + // Throw if there was any errors initializing startup + capture.Throw(); + }; + }); + } + } + }); + } + + private void ExecuteHostingStartups() + { + var webHostOptions = new WebHostOptions(_config, Assembly.GetEntryAssembly()?.GetName().Name); + + if (webHostOptions.PreventHostingStartup) + { + return; + } + + var exceptions = new List(); + _hostingStartupWebHostBuilder = new HostingStartupWebHostBuilder(this); + + // Execute the hosting startup assemblies + foreach (var assemblyName in webHostOptions.GetFinalHostingStartupAssemblies().Distinct(StringComparer.OrdinalIgnoreCase)) + { + try + { + var assembly = Assembly.Load(new AssemblyName(assemblyName)); + + foreach (var attribute in assembly.GetCustomAttributes()) + { + var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType); + hostingStartup.Configure(_hostingStartupWebHostBuilder); + } + } + catch (Exception ex) + { + // Capture any errors that happen during startup + exceptions.Add(new InvalidOperationException($"Startup assembly {assemblyName} failed to execute. See the inner exception for more details.", ex)); + } + } + + if (exceptions.Count > 0) + { + _hostingStartupErrors = new AggregateException(exceptions); + } + } + + public IWebHost Build() + { + throw new NotSupportedException($"Building this implementation of {nameof(IWebHostBuilder)} is not supported."); + } + + public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _builder.ConfigureAppConfiguration((context, builder) => + { + var webhostBuilderContext = GetWebHostBuilderContext(context); + configureDelegate(webhostBuilderContext, builder); + }); + + return this; + } + + public IWebHostBuilder ConfigureServices(Action configureServices) + { + return ConfigureServices((context, services) => configureServices(services)); + } + + public IWebHostBuilder ConfigureServices(Action configureServices) + { + _builder.ConfigureServices((context, builder) => + { + var webhostBuilderContext = GetWebHostBuilderContext(context); + configureServices(webhostBuilderContext, builder); + }); + + return this; + } + + public IWebHostBuilder UseDefaultServiceProvider(Action configure) + { + // REVIEW: This is a hack to change the builder with the HostBuilderContext in scope, + // we're not actually using configuration here + _builder.ConfigureAppConfiguration((context, _) => + { + var webHostBuilderContext = GetWebHostBuilderContext(context); + var options = new ServiceProviderOptions(); + configure(webHostBuilderContext, options); + + // This is only fine because this runs last + _builder.UseServiceProviderFactory(new DefaultServiceProviderFactory(options)); + }); + + return this; + } + + public IWebHostBuilder UseStartup(Type startupType) + { + _builder.ConfigureServices((context, services) => + { + UseStartup(startupType, context, services); + }); + + return this; + } + + private void UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services) + { + var webHostBuilderContext = GetWebHostBuilderContext(context); + var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)]; + + ExceptionDispatchInfo startupError = null; + object instance = null; + ConfigureBuilder configureBuilder = null; + + try + { + // We cannot support methods that return IServiceProvider as that is terminal and we need ConfigureServices to compose + if (typeof(IStartup).IsAssignableFrom(startupType)) + { + throw new NotSupportedException($"{typeof(IStartup)} isn't supported"); + } + + instance = ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType); + context.Properties[_startupKey] = instance; + + // Startup.ConfigureServices + var configureServicesBuilder = StartupLoader.FindConfigureServicesDelegate(startupType, context.HostingEnvironment.EnvironmentName); + var configureServices = configureServicesBuilder.Build(instance); + + configureServices(services); + + // REVIEW: We're doing this in the callback so that we have access to the hosting environment + // Startup.ConfigureContainer + var configureContainerBuilder = StartupLoader.FindConfigureContainerDelegate(startupType, context.HostingEnvironment.EnvironmentName); + if (configureContainerBuilder.MethodInfo != null) + { + var containerType = configureContainerBuilder.GetContainerType(); + // Store the builder in the property bag + _builder.Properties[typeof(ConfigureContainerBuilder)] = configureContainerBuilder; + + var actionType = typeof(Action<,>).MakeGenericType(typeof(HostBuilderContext), containerType); + + // Get the private ConfigureContainer method on this type then close over the container type + var configureCallback = GetType().GetMethod(nameof(ConfigureContainer), BindingFlags.NonPublic | BindingFlags.Instance) + .MakeGenericMethod(containerType) + .CreateDelegate(actionType, this); + + // _builder.ConfigureContainer(ConfigureContainer); + typeof(IHostBuilder).GetMethods().First(m => m.Name == nameof(IHostBuilder.ConfigureContainer)) + .MakeGenericMethod(containerType) + .Invoke(_builder, new object[] { configureCallback }); + } + + // Resolve Configure after calling ConfigureServices and ConfigureContainer + configureBuilder = StartupLoader.FindConfigureDelegate(startupType, context.HostingEnvironment.EnvironmentName); + } + catch (Exception ex) when (webHostOptions.CaptureStartupErrors) + { + startupError = ExceptionDispatchInfo.Capture(ex); + } + + // Startup.Configure + services.Configure(options => + { + options.ConfigureApplication = app => + { + // Throw if there was any errors initializing startup + startupError?.Throw(); + + // Execute Startup.Configure + if (instance != null && configureBuilder != null) + { + configureBuilder.Build(instance)(app); + } + }; + }); + } + + private void ConfigureContainer(HostBuilderContext context, TContainer container) + { + var instance = context.Properties[_startupKey]; + var builder = (ConfigureContainerBuilder)context.Properties[typeof(ConfigureContainerBuilder)]; + builder.Build(instance)(container); + } + + public IWebHostBuilder Configure(Action configure) + { + _builder.ConfigureServices((context, services) => + { + services.Configure(options => + { + options.ConfigureApplication = configure; + }); + }); + + return this; + } + + private WebHostBuilderContext GetWebHostBuilderContext(HostBuilderContext context) + { + if (!context.Properties.TryGetValue(typeof(WebHostBuilderContext), out var contextVal)) + { + var options = new WebHostOptions(context.Configuration, Assembly.GetEntryAssembly()?.GetName().Name); + var hostingEnvironment = new HostingEnvironment(); + hostingEnvironment.Initialize(context.HostingEnvironment.ContentRootPath, options); + + var webHostBuilderContext = new WebHostBuilderContext + { + Configuration = context.Configuration, + HostingEnvironment = hostingEnvironment + }; + context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext; + context.Properties[typeof(WebHostOptions)] = options; + return webHostBuilderContext; + } + + return (WebHostBuilderContext)contextVal; + } + + public string GetSetting(string key) + { + return _config[key]; + } + + public IWebHostBuilder UseSetting(string key, string value) + { + _config[key] = value; + return this; + } + + // This exists just so that we can use ActivatorUtilities.CreateInstance on the Startup class + private class HostServiceProvider : IServiceProvider + { + private readonly WebHostBuilderContext _context; + + public HostServiceProvider(WebHostBuilderContext context) + { + _context = context; + } + + public object GetService(Type serviceType) + { + // The implementation of the HostingEnvironment supports both interfaces + if (serviceType == typeof(Microsoft.AspNetCore.Hosting.IHostingEnvironment) || serviceType == typeof(IHostingEnvironment)) + { + return _context.HostingEnvironment; + } + + if (serviceType == typeof(IConfiguration)) + { + return _context.Configuration; + } + + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostServiceOptions.cs b/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostServiceOptions.cs new file mode 100644 index 00000000..715c4351 --- /dev/null +++ b/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostServiceOptions.cs @@ -0,0 +1,17 @@ +// 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 System; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal class GenericWebHostServiceOptions + { + public Action ConfigureApplication { get; set; } + + public WebHostOptions WebHostOptions { get; set; } + + public AggregateException HostingStartupExceptions { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostedService.cs b/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostedService.cs new file mode 100644 index 00000000..8ec39059 --- /dev/null +++ b/src/Microsoft.AspNetCore.Hosting/GenericHost/GenericWebHostedService.cs @@ -0,0 +1,201 @@ +// 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 System; +using System.Collections.Generic; +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; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Hosting.Views; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.StackTrace.Sources; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal class GenericWebHostService : IHostedService + { + public GenericWebHostService(IOptions options, + IServer server, + ILogger logger, + DiagnosticListener diagnosticListener, + IHttpContextFactory httpContextFactory, + IApplicationBuilderFactory applicationBuilderFactory, + IEnumerable startupFilters, + IConfiguration configuration, + IHostingEnvironment hostingEnvironment) + { + Options = options.Value; + Server = server; + Logger = logger; + DiagnosticListener = diagnosticListener; + HttpContextFactory = httpContextFactory; + ApplicationBuilderFactory = applicationBuilderFactory; + StartupFilters = startupFilters; + Configuration = configuration; + HostingEnvironment = hostingEnvironment; + } + + public GenericWebHostServiceOptions Options { get; } + public IServer Server { get; } + public ILogger Logger { get; } + public DiagnosticListener DiagnosticListener { get; } + public IHttpContextFactory HttpContextFactory { get; } + public IApplicationBuilderFactory ApplicationBuilderFactory { get; } + public IEnumerable StartupFilters { get; } + public IConfiguration Configuration { get; } + public IHostingEnvironment HostingEnvironment { get; } + + public async Task StartAsync(CancellationToken cancellationToken) + { + HostingEventSource.Log.HostStart(); + + var serverAddressesFeature = Server.Features?.Get(); + var addresses = serverAddressesFeature?.Addresses; + if (addresses != null && !addresses.IsReadOnly && addresses.Count == 0) + { + var urls = Configuration[WebHostDefaults.ServerUrlsKey]; + if (!string.IsNullOrEmpty(urls)) + { + serverAddressesFeature.PreferHostingUrls = WebHostUtilities.ParseBool(Configuration, WebHostDefaults.PreferHostingUrlsKey); + + foreach (var value in urls.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + addresses.Add(value); + } + } + } + + RequestDelegate application = null; + + try + { + Action configure = Options.ConfigureApplication; + + if (configure == null) + { + throw new InvalidOperationException($"No application configured. Please specify an application via IWebHostBuilder.UseStartup, IWebHostBuilder.Configure, or specifying the startup assembly via {nameof(WebHostDefaults.StartupAssemblyKey)} in the web host configuration."); + } + + var builder = ApplicationBuilderFactory.CreateBuilder(Server.Features); + + foreach (var filter in StartupFilters.Reverse()) + { + configure = filter.Configure(configure); + } + + configure(builder); + + // Build the request pipeline + application = builder.Build(); + } + catch (Exception ex) + { + Logger.ApplicationError(ex); + + if (!Options.WebHostOptions.CaptureStartupErrors) + { + throw; + } + + application = BuildErrorPageApplication(ex); + } + + var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, HttpContextFactory); + + await Server.StartAsync(httpApplication, cancellationToken); + + if (addresses != null) + { + foreach (var address in addresses) + { + Logger.LogInformation("Now listening on: {address}", address); + } + } + + if (Logger.IsEnabled(LogLevel.Debug)) + { + foreach (var assembly in Options.WebHostOptions.GetFinalHostingStartupAssemblies()) + { + Logger.LogDebug("Loaded hosting startup assembly {assemblyName}", assembly); + } + } + + if (Options.HostingStartupExceptions != null) + { + foreach (var exception in Options.HostingStartupExceptions.InnerExceptions) + { + Logger.HostingStartupAssemblyError(exception); + } + } + } + + private RequestDelegate BuildErrorPageApplication(Exception exception) + { + if (exception is TargetInvocationException tae) + { + exception = tae.InnerException; + } + + var showDetailedErrors = HostingEnvironment.IsDevelopment() || Options.WebHostOptions.DetailedErrors; + + var model = new ErrorPageModel + { + RuntimeDisplayName = RuntimeInformation.FrameworkDescription + }; + var systemRuntimeAssembly = typeof(System.ComponentModel.DefaultValueAttribute).GetTypeInfo().Assembly; + var assemblyVersion = new AssemblyName(systemRuntimeAssembly.FullName).Version.ToString(); + var clrVersion = assemblyVersion; + model.RuntimeArchitecture = RuntimeInformation.ProcessArchitecture.ToString(); + var currentAssembly = typeof(ErrorPage).GetTypeInfo().Assembly; + model.CurrentAssemblyVesion = currentAssembly + .GetCustomAttribute() + .InformationalVersion; + model.ClrVersion = clrVersion; + model.OperatingSystemDescription = RuntimeInformation.OSDescription; + + if (showDetailedErrors) + { + var exceptionDetailProvider = new ExceptionDetailsProvider( + HostingEnvironment.ContentRootFileProvider, + sourceCodeLineCount: 6); + + model.ErrorDetails = exceptionDetailProvider.GetDetails(exception); + } + else + { + model.ErrorDetails = new ExceptionDetails[0]; + } + + var errorPage = new ErrorPage(model); + return context => + { + context.Response.StatusCode = 500; + context.Response.Headers["Cache-Control"] = "no-cache"; + return errorPage.ExecuteAsync(context); + }; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + try + { + await Server.StopAsync(cancellationToken); + } + finally + { + HostingEventSource.Log.HostStop(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Hosting/GenericHost/HostingStartupWebHostBuilder.cs b/src/Microsoft.AspNetCore.Hosting/GenericHost/HostingStartupWebHostBuilder.cs new file mode 100644 index 00000000..52eaec81 --- /dev/null +++ b/src/Microsoft.AspNetCore.Hosting/GenericHost/HostingStartupWebHostBuilder.cs @@ -0,0 +1,79 @@ +// 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 System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + // We use this type to capture calls to the IWebHostBuilder so the we can properly order calls to + // to GenericHostWebHostBuilder. + internal class HostingStartupWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider + { + private readonly GenericWebHostBuilder _builder; + private Action _configureConfiguration; + private Action _configureServices; + + public HostingStartupWebHostBuilder(GenericWebHostBuilder builder) + { + _builder = builder; + } + + public IWebHost Build() + { + throw new NotSupportedException($"Building this implementation of {nameof(IWebHostBuilder)} is not supported."); + } + + public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _configureConfiguration += configureDelegate; + return this; + } + + public IWebHostBuilder ConfigureServices(Action configureServices) + { + return ConfigureServices((context, services) => configureServices(services)); + } + + public IWebHostBuilder ConfigureServices(Action configureServices) + { + _configureServices += configureServices; + return this; + } + + public string GetSetting(string key) => _builder.GetSetting(key); + + public IWebHostBuilder UseSetting(string key, string value) + { + _builder.UseSetting(key, value); + return this; + } + + public void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) + { + _configureServices?.Invoke(context, services); + } + + public void ConfigureAppConfiguration(WebHostBuilderContext context, IConfigurationBuilder builder) + { + _configureConfiguration?.Invoke(context, builder); + } + + public IWebHostBuilder UseDefaultServiceProvider(Action configure) + { + return _builder.UseDefaultServiceProvider(configure); + } + + public IWebHostBuilder Configure(Action configure) + { + return _builder.Configure(configure); + } + + public IWebHostBuilder UseStartup(Type startupType) + { + return _builder.UseStartup(startupType); + } + } +} diff --git a/src/Microsoft.AspNetCore.Hosting/GenericHost/ISupportsStartup.cs b/src/Microsoft.AspNetCore.Hosting/GenericHost/ISupportsStartup.cs new file mode 100644 index 00000000..16322c8b --- /dev/null +++ b/src/Microsoft.AspNetCore.Hosting/GenericHost/ISupportsStartup.cs @@ -0,0 +1,14 @@ +// 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 System; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal interface ISupportsStartup + { + IWebHostBuilder Configure(Action configure); + IWebHostBuilder UseStartup(Type startupType); + } +} diff --git a/src/Microsoft.AspNetCore.Hosting/GenericHost/ISupportsUseDefaultServiceProvider.cs b/src/Microsoft.AspNetCore.Hosting/GenericHost/ISupportsUseDefaultServiceProvider.cs new file mode 100644 index 00000000..bf9813cd --- /dev/null +++ b/src/Microsoft.AspNetCore.Hosting/GenericHost/ISupportsUseDefaultServiceProvider.cs @@ -0,0 +1,13 @@ +// 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 System; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Internal +{ + internal interface ISupportsUseDefaultServiceProvider + { + IWebHostBuilder UseDefaultServiceProvider(Action configure); + } +} diff --git a/src/Microsoft.AspNetCore.Hosting/GenericHostWebHostBuilderExtensions.cs b/src/Microsoft.AspNetCore.Hosting/GenericHostWebHostBuilderExtensions.cs new file mode 100644 index 00000000..ea903ad7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Hosting/GenericHostWebHostBuilderExtensions.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; + +namespace Microsoft.Extensions.Hosting +{ + public static class GenericHostWebHostBuilderExtensions + { + public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action configure) + { + var webhostBuilder = new GenericWebHostBuilder(builder); + configure(webhostBuilder); + return builder; + } + } +} diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/ConfigureContainerBuilder.cs b/src/Microsoft.AspNetCore.Hosting/Internal/ConfigureContainerBuilder.cs index ed8d0fd0..8791a918 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/ConfigureContainerBuilder.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/ConfigureContainerBuilder.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal public MethodInfo MethodInfo { get; } - public Func, Action> ConfigureContainerFilters { get; set; } + public Func, Action> ConfigureContainerFilters { get; set; } = f => f; public Action Build(object instance) => container => Invoke(instance, container); diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/ConfigureServicesBuilder.cs b/src/Microsoft.AspNetCore.Hosting/Internal/ConfigureServicesBuilder.cs index 4206d0d6..cf9a6932 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/ConfigureServicesBuilder.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/ConfigureServicesBuilder.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal public MethodInfo MethodInfo { get; } - public Func, Func> StartupServiceFilters { get; set; } + public Func, Func> StartupServiceFilters { get; set; } = f => f; public Func Build(object instance) => services => Invoke(instance, services); diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/StartupLoader.cs b/src/Microsoft.AspNetCore.Hosting/Internal/StartupLoader.cs index d7211d39..72dab817 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/StartupLoader.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/StartupLoader.cs @@ -267,19 +267,19 @@ namespace Microsoft.AspNetCore.Hosting.Internal return type; } - private static ConfigureBuilder FindConfigureDelegate(Type startupType, string environmentName) + internal static ConfigureBuilder FindConfigureDelegate(Type startupType, string environmentName) { var configureMethod = FindMethod(startupType, "Configure{0}", environmentName, typeof(void), required: true); return new ConfigureBuilder(configureMethod); } - private static ConfigureContainerBuilder FindConfigureContainerDelegate(Type startupType, string environmentName) + internal static ConfigureContainerBuilder FindConfigureContainerDelegate(Type startupType, string environmentName) { var configureMethod = FindMethod(startupType, "Configure{0}Container", environmentName, typeof(void), required: false); return new ConfigureContainerBuilder(configureMethod); } - private static ConfigureServicesBuilder FindConfigureServicesDelegate(Type startupType, string environmentName) + internal static ConfigureServicesBuilder FindConfigureServicesDelegate(Type startupType, string environmentName) { var servicesMethod = FindMethod(startupType, "Configure{0}Services", environmentName, typeof(IServiceProvider), required: false) ?? FindMethod(startupType, "Configure{0}Services", environmentName, typeof(void), required: false); diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs b/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs index 3764427f..0ff44958 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs @@ -192,7 +192,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal if (_startup == null) { - throw new InvalidOperationException($"No startup configured. Please specify startup via WebHostBuilder.UseStartup, WebHostBuilder.Configure, injecting {nameof(IStartup)} or specifying the startup assembly via {nameof(WebHostDefaults.StartupAssemblyKey)} in the web host configuration."); + throw new InvalidOperationException($"No application configured. Please specify startup via IWebHostBuilder.UseStartup, IWebHostBuilder.Configure, injecting {nameof(IStartup)} or specifying the startup assembly via {nameof(WebHostDefaults.StartupAssemblyKey)} in the web host configuration."); } } diff --git a/src/Microsoft.AspNetCore.Hosting/WebHostBuilderExtensions.cs b/src/Microsoft.AspNetCore.Hosting/WebHostBuilderExtensions.cs index 09c7e6d9..63dd3257 100644 --- a/src/Microsoft.AspNetCore.Hosting/WebHostBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Hosting/WebHostBuilderExtensions.cs @@ -29,15 +29,21 @@ namespace Microsoft.AspNetCore.Hosting var startupAssemblyName = configureApp.GetMethodInfo().DeclaringType.GetTypeInfo().Assembly.GetName().Name; - return hostBuilder - .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) - .ConfigureServices(services => + hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName); + + // Light up the ISupportsStartup implementation + if (hostBuilder is ISupportsStartup supportsStartup) + { + return supportsStartup.Configure(configureApp); + } + + return hostBuilder.ConfigureServices(services => + { + services.AddSingleton(sp => { - services.AddSingleton(sp => - { - return new DelegateStartup(sp.GetRequiredService>(), configureApp); - }); + return new DelegateStartup(sp.GetRequiredService>(), configureApp); }); + }); } @@ -51,8 +57,15 @@ namespace Microsoft.AspNetCore.Hosting { var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name; + hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName); + + // Light up the GenericWebHostBuilder implementation + if (hostBuilder is ISupportsStartup supportsStartup) + { + return supportsStartup.UseStartup(startupType); + } + return hostBuilder - .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) .ConfigureServices(services => { if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) @@ -100,6 +113,12 @@ namespace Microsoft.AspNetCore.Hosting /// The . public static IWebHostBuilder UseDefaultServiceProvider(this IWebHostBuilder hostBuilder, Action configure) { + // Light up the GenericWebHostBuilder implementation + if (hostBuilder is ISupportsUseDefaultServiceProvider supportsDefaultServiceProvider) + { + return supportsDefaultServiceProvider.UseDefaultServiceProvider(configure); + } + return hostBuilder.ConfigureServices((context, services) => { var options = new ServiceProviderOptions(); diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/Fakes/GenericWebHost.cs b/test/Microsoft.AspNetCore.Hosting.Tests/Fakes/GenericWebHost.cs new file mode 100644 index 00000000..d61ce147 --- /dev/null +++ b/test/Microsoft.AspNetCore.Hosting.Tests/Fakes/GenericWebHost.cs @@ -0,0 +1,35 @@ +// 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 System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.Hosting.Tests.Fakes +{ + internal class GenericWebHost : IWebHost + { + private readonly IHost _host; + + public GenericWebHost(IHost host) + { + _host = host; + } + + public IFeatureCollection ServerFeatures => Services.GetRequiredService().Features; + + public IServiceProvider Services => _host.Services; + + public void Dispose() => _host.Dispose(); + + public void Start() => _host.Start(); + + public Task StartAsync(CancellationToken cancellationToken = default) => _host.StartAsync(cancellationToken); + + public Task StopAsync(CancellationToken cancellationToken = default) => _host.StopAsync(cancellationToken); + } +} diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/Fakes/GenericWebHostBuilderWrapper.cs b/test/Microsoft.AspNetCore.Hosting.Tests/Fakes/GenericWebHostBuilderWrapper.cs new file mode 100644 index 00000000..3ff3aeef --- /dev/null +++ b/test/Microsoft.AspNetCore.Hosting.Tests/Fakes/GenericWebHostBuilderWrapper.cs @@ -0,0 +1,77 @@ +// 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 System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.Hosting.Tests.Fakes +{ + public class GenericWebHostBuilderWrapper : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider + { + private readonly GenericWebHostBuilder _builder; + private readonly HostBuilder _hostBuilder; + + internal GenericWebHostBuilderWrapper(HostBuilder hostBuilder) + { + _builder = new GenericWebHostBuilder(hostBuilder); + _hostBuilder = hostBuilder; + } + + // This is the only one that doesn't pass through + public IWebHost Build() + { + return new GenericWebHost(_hostBuilder.Build()); + } + + public IWebHostBuilder Configure(Action configure) + { + _builder.Configure(configure); + return this; + } + + public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _builder.ConfigureAppConfiguration(configureDelegate); + return this; + } + + public IWebHostBuilder ConfigureServices(Action configureServices) + { + _builder.ConfigureServices(configureServices); + return this; + } + + public IWebHostBuilder ConfigureServices(Action configureServices) + { + _builder.ConfigureServices(configureServices); + return this; + } + + public string GetSetting(string key) + { + return _builder.GetSetting(key); + } + + public IWebHostBuilder UseDefaultServiceProvider(Action configure) + { + _builder.UseDefaultServiceProvider(configure); + return this; + } + + public IWebHostBuilder UseSetting(string key, string value) + { + _builder.UseSetting(key, value); + return this; + } + + public IWebHostBuilder UseStartup(Type startupType) + { + _builder.UseStartup(startupType); + return this; + } + } +} diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/Fakes/StartupNoServicesNoInterface.cs b/test/Microsoft.AspNetCore.Hosting.Tests/Fakes/StartupNoServicesNoInterface.cs new file mode 100644 index 00000000..d97cee73 --- /dev/null +++ b/test/Microsoft.AspNetCore.Hosting.Tests/Fakes/StartupNoServicesNoInterface.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting.Fakes +{ + public class StartupNoServicesNoInterface + { + public void ConfigureServices(IServiceCollection services) + { + + } + + public void Configure(IApplicationBuilder app) + { + + } + } +} diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/Microsoft.AspNetCore.Hosting.Tests.csproj b/test/Microsoft.AspNetCore.Hosting.Tests/Microsoft.AspNetCore.Hosting.Tests.csproj index 5b24d9e6..5f08fb9b 100644 --- a/test/Microsoft.AspNetCore.Hosting.Tests/Microsoft.AspNetCore.Hosting.Tests.csproj +++ b/test/Microsoft.AspNetCore.Hosting.Tests/Microsoft.AspNetCore.Hosting.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs index c1244e5c..876959dd 100644 --- a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs +++ b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs @@ -13,10 +13,12 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Fakes; using Microsoft.AspNetCore.Hosting.Internal; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Tests.Fakes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; @@ -29,22 +31,24 @@ namespace Microsoft.AspNetCore.Hosting { public class WebHostBuilderTests { - [Fact] - public void Build_honors_UseStartup_with_string() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_honors_UseStartup_with_string(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder().UseServer(new TestServer()); + builder = builder.UseServer(new TestServer()); - using (var host = (WebHost)builder.UseStartup("MyStartupAssembly").Build()) + using (var host = builder.UseStartup("MyStartupAssembly").Build()) { - Assert.Equal("MyStartupAssembly", host.Options.ApplicationName); - Assert.Equal("MyStartupAssembly", host.Options.StartupAssembly); + var options = new WebHostOptions(host.Services.GetRequiredService()); + Assert.Equal("MyStartupAssembly", options.ApplicationName); + Assert.Equal("MyStartupAssembly", options.StartupAssembly); } } - [Fact] - public async Task StartupMissing_Fallback() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task StartupMissing_Fallback(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder(); var server = new TestServer(); using (var host = builder.UseServer(server).UseStartup("MissingStartupAssembly").Build()) { @@ -53,10 +57,10 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public async Task StartupStaticCtorThrows_Fallback() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task StartupStaticCtorThrows_Fallback(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder(); var server = new TestServer(); var host = builder.UseServer(server).UseStartup().Build(); using (host) @@ -66,10 +70,10 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public async Task StartupCtorThrows_Fallback() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task StartupCtorThrows_Fallback(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder(); var server = new TestServer(); var host = builder.UseServer(server).UseStartup().Build(); using (host) @@ -79,10 +83,10 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public async Task StartupCtorThrows_TypeLoadException() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task StartupCtorThrows_TypeLoadException(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder(); var server = new TestServer(); var host = builder.UseServer(server).UseStartup().Build(); using (host) @@ -92,10 +96,10 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public async Task IApplicationLifetimeRegisteredEvenWhenStartupCtorThrows_Fallback() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task IApplicationLifetimeRegisteredEvenWhenStartupCtorThrows_Fallback(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder(); var server = new TestServer(); var host = builder.UseServer(server).UseStartup().Build(); using (host) @@ -109,11 +113,12 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public async Task DefaultObjectPoolProvider_IsRegistered() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task DefaultObjectPoolProvider_IsRegistered(IWebHostBuilder builder) { var server = new TestServer(); - var host = CreateWebHostBuilder() + var host = builder .UseServer(server) .Configure(app => { }) .Build(); @@ -124,10 +129,10 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public async Task StartupConfigureServicesThrows_Fallback() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task StartupConfigureServicesThrows_Fallback(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder(); var server = new TestServer(); var host = builder.UseServer(server).UseStartup().Build(); using (host) @@ -137,10 +142,10 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public async Task StartupConfigureThrows_Fallback() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task StartupConfigureThrows_Fallback(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder(); var server = new TestServer(); var host = builder.UseServer(server).UseStartup().Build(); using (host) @@ -150,23 +155,25 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void DefaultCreatesLoggerFactory() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void DefaultCreatesLoggerFactory(IWebHostBuilder builder) { - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseServer(new TestServer()) .UseStartup(); - using (var host = (WebHost)hostBuilder.Build()) + using (var host = hostBuilder.Build()) { Assert.NotNull(host.Services.GetService()); } } - [Fact] - public void ConfigureDefaultServiceProvider() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void ConfigureDefaultServiceProvider(IWebHostBuilder builder) { - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseServer(new TestServer()) .ConfigureServices(s => { @@ -185,11 +192,12 @@ namespace Microsoft.AspNetCore.Hosting Assert.Throws(() => hostBuilder.Build().Start()); } - [Fact] - public void ConfigureDefaultServiceProviderWithContext() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void ConfigureDefaultServiceProviderWithContext(IWebHostBuilder builder) { var configurationCallbackCalled = false; - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseServer(new TestServer()) .ConfigureServices(s => { @@ -212,11 +220,12 @@ namespace Microsoft.AspNetCore.Hosting Assert.True(configurationCallbackCalled); } - [Fact] - public void MultipleConfigureLoggingInvokedInOrder() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void MultipleConfigureLoggingInvokedInOrder(IWebHostBuilder builder) { var callCount = 0; //Verify ordering - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .ConfigureLogging(loggerFactory => { Assert.Equal(0, callCount++); @@ -234,8 +243,9 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public async Task MultipleStartupAssembliesSpecifiedOnlyAddAssemblyOnce() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task MultipleStartupAssembliesSpecifiedOnlyAddAssemblyOnce(IWebHostBuilder builder) { var provider = new TestLoggerProvider(); var assemblyName = "RandomName"; @@ -246,7 +256,7 @@ namespace Microsoft.AspNetCore.Hosting }; var config = new ConfigurationBuilder().AddInMemoryCollection(data).Build(); - var builder = CreateWebHostBuilder() + builder = builder .UseConfiguration(config) .ConfigureLogging((_, factory) => { @@ -255,7 +265,7 @@ namespace Microsoft.AspNetCore.Hosting .UseServer(new TestServer()); // Verify that there was only one exception throw rather than two. - using (var host = (WebHost)builder.Build()) + using (var host = builder.Build()) { await host.StartAsync(); var context = provider.Sink.Writes.Where(s => s.EventId.Id == LoggerEventIds.HostingStartupAssemblyException); @@ -267,7 +277,7 @@ namespace Microsoft.AspNetCore.Hosting [Fact] public void HostingContextContainsAppConfigurationDuringConfigureLogging() { - var hostBuilder = new WebHostBuilder() + var hostBuilder = CreateWebHostBuilder() .ConfigureAppConfiguration((context, configBuilder) => configBuilder.AddInMemoryCollection( new KeyValuePair[] @@ -287,7 +297,7 @@ namespace Microsoft.AspNetCore.Hosting [Fact] public void HostingContextContainsAppConfigurationDuringConfigureServices() { - var hostBuilder = new WebHostBuilder() + var hostBuilder = CreateWebHostBuilder() .ConfigureAppConfiguration((context, configBuilder) => configBuilder.AddInMemoryCollection( new KeyValuePair[] @@ -304,23 +314,25 @@ namespace Microsoft.AspNetCore.Hosting using (hostBuilder.Build()) { } } - [Fact] - public void ThereIsAlwaysConfiguration() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void ThereIsAlwaysConfiguration(IWebHostBuilder builder) { - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseServer(new TestServer()) .UseStartup(); - using (var host = (WebHost)hostBuilder.Build()) + using (var host = hostBuilder.Build()) { Assert.NotNull(host.Services.GetService()); } } - [Fact] - public void ConfigureConfigurationSettingsPropagated() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void ConfigureConfigurationSettingsPropagated(IWebHostBuilder builder) { - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseSetting("key1", "value1") .ConfigureAppConfiguration((context, configBuilder) => { @@ -333,10 +345,11 @@ namespace Microsoft.AspNetCore.Hosting using (hostBuilder.Build()) { } } - [Fact] - public void CanConfigureConfigurationAndRetrieveFromDI() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void CanConfigureConfigurationAndRetrieveFromDI(IWebHostBuilder builder) { - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .ConfigureAppConfiguration((_, configBuilder) => { configBuilder @@ -350,7 +363,7 @@ namespace Microsoft.AspNetCore.Hosting .UseServer(new TestServer()) .UseStartup(); - using (var host = (WebHost)hostBuilder.Build()) + using (var host = hostBuilder.Build()) { var config = host.Services.GetService(); Assert.NotNull(config); @@ -358,10 +371,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void DoNotCaptureStartupErrorsByDefault() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void DoNotCaptureStartupErrorsByDefault(IWebHostBuilder builder) { - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseServer(new TestServer()) .UseStartup(); @@ -386,10 +400,11 @@ namespace Microsoft.AspNetCore.Hosting Assert.True(service.Disposed); } - [Fact] - public void CaptureStartupErrorsHonored() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void CaptureStartupErrorsHonored(IWebHostBuilder builder) { - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .CaptureStartupErrors(false) .UseServer(new TestServer()) .UseStartup(); @@ -398,11 +413,12 @@ namespace Microsoft.AspNetCore.Hosting Assert.Equal("A public method named 'ConfigureProduction' or 'Configure' could not be found in the 'Microsoft.AspNetCore.Hosting.Fakes.StartupBoom' type.", exception.Message); } - [Fact] - public void ConfigureServices_CanBeCalledMultipleTimes() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void ConfigureServices_CanBeCalledMultipleTimes(IWebHostBuilder builder) { var callCount = 0; // Verify ordering - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseServer(new TestServer()) .ConfigureServices(services => { @@ -425,23 +441,26 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void CodeBasedSettingsCodeBasedOverride() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void CodeBasedSettingsCodeBasedOverride(IWebHostBuilder builder) { - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseSetting(WebHostDefaults.EnvironmentKey, "EnvA") .UseSetting(WebHostDefaults.EnvironmentKey, "EnvB") .UseServer(new TestServer()) .UseStartup(); - using (var host = (WebHost)hostBuilder.Build()) + using (var host = hostBuilder.Build()) { - Assert.Equal("EnvB", host.Options.Environment); + var options = new WebHostOptions(host.Services.GetRequiredService()); + Assert.Equal("EnvB", options.Environment); } } - [Fact] - public void CodeBasedSettingsConfigBasedOverride() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void CodeBasedSettingsConfigBasedOverride(IWebHostBuilder builder) { var settings = new Dictionary { @@ -452,20 +471,22 @@ namespace Microsoft.AspNetCore.Hosting .AddInMemoryCollection(settings) .Build(); - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseSetting(WebHostDefaults.EnvironmentKey, "EnvA") .UseConfiguration(config) .UseServer(new TestServer()) .UseStartup(); - using (var host = (WebHost)hostBuilder.Build()) + using (var host = hostBuilder.Build()) { - Assert.Equal("EnvB", host.Options.Environment); + var options = new WebHostOptions(host.Services.GetRequiredService()); + Assert.Equal("EnvB", options.Environment); } } - [Fact] - public void ConfigBasedSettingsCodeBasedOverride() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void ConfigBasedSettingsCodeBasedOverride(IWebHostBuilder builder) { var settings = new Dictionary { @@ -476,20 +497,22 @@ namespace Microsoft.AspNetCore.Hosting .AddInMemoryCollection(settings) .Build(); - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseConfiguration(config) .UseSetting(WebHostDefaults.EnvironmentKey, "EnvB") .UseServer(new TestServer()) .UseStartup(); - using (var host = (WebHost)hostBuilder.Build()) + using (var host = hostBuilder.Build()) { - Assert.Equal("EnvB", host.Options.Environment); + var options = new WebHostOptions(host.Services.GetRequiredService()); + Assert.Equal("EnvB", options.Environment); } } - [Fact] - public void ConfigBasedSettingsConfigBasedOverride() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void ConfigBasedSettingsConfigBasedOverride(IWebHostBuilder builder) { var settings = new Dictionary { @@ -509,33 +532,35 @@ namespace Microsoft.AspNetCore.Hosting .AddInMemoryCollection(overrideSettings) .Build(); - var hostBuilder = new WebHostBuilder() + var hostBuilder = builder .UseConfiguration(config) .UseConfiguration(overrideConfig) .UseServer(new TestServer()) .UseStartup(); - using (var host = (WebHost)hostBuilder.Build()) + using (var host = hostBuilder.Build()) { - Assert.Equal("EnvB", host.Options.Environment); + var options = new WebHostOptions(host.Services.GetRequiredService()); + Assert.Equal("EnvB", options.Environment); } } - [Fact] - public void UseEnvironmentIsNotOverriden() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void UseEnvironmentIsNotOverriden(IWebHostBuilder builder) { var vals = new Dictionary { { "ENV", "Dev" }, }; - var builder = new ConfigurationBuilder() + var configBuilder = new ConfigurationBuilder() .AddInMemoryCollection(vals); - var config = builder.Build(); + var config = configBuilder.Build(); var expected = "MY_TEST_ENVIRONMENT"; - using (var host = new WebHostBuilder() + using (var host = builder .UseConfiguration(config) .UseEnvironment(expected) .UseServer(new TestServer()) @@ -547,19 +572,20 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void BuildAndDispose() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void BuildAndDispose(IWebHostBuilder builder) { var vals = new Dictionary { { "ENV", "Dev" }, }; - var builder = new ConfigurationBuilder() + var configBuilder = new ConfigurationBuilder() .AddInMemoryCollection(vals); - var config = builder.Build(); + var config = configBuilder.Build(); var expected = "MY_TEST_ENVIRONMENT"; - using (var host = new WebHostBuilder() + using (var host = builder .UseConfiguration(config) .UseEnvironment(expected) .UseServer(new TestServer()) @@ -567,18 +593,19 @@ namespace Microsoft.AspNetCore.Hosting .Build()) { } } - [Fact] - public void UseBasePathConfiguresBasePath() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void UseBasePathConfiguresBasePath(IWebHostBuilder builder) { var vals = new Dictionary { { "ENV", "Dev" }, }; - var builder = new ConfigurationBuilder() + var configBuilder = new ConfigurationBuilder() .AddInMemoryCollection(vals); - var config = builder.Build(); + var config = configBuilder.Build(); - using (var host = new WebHostBuilder() + using (var host = builder .UseConfiguration(config) .UseContentRoot("/") .UseServer(new TestServer()) @@ -590,10 +617,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void RelativeContentRootIsResolved() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void RelativeContentRootIsResolved(IWebHostBuilder builder) { - using (var host = new WebHostBuilder() + using (var host = builder .UseContentRoot("testroot") .UseServer(new TestServer()) .UseStartup("Microsoft.AspNetCore.Hosting.Tests") @@ -610,10 +638,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void DefaultContentRootIsApplicationBasePath() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void DefaultContentRootIsApplicationBasePath(IWebHostBuilder builder) { - using (var host = new WebHostBuilder() + using (var host = builder .UseServer(new TestServer()) .UseStartup("Microsoft.AspNetCore.Hosting.Tests") .Build()) @@ -624,22 +653,23 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void DefaultWebHostBuilderWithNoStartupThrows() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void DefaultWebHostBuilderWithNoStartupThrows(IWebHostBuilder builder) { - var host = new WebHostBuilder() + var host = builder .UseServer(new TestServer()); - var ex = Assert.Throws(() => host.Build()); + var ex = Assert.Throws(() => host.Build().Start()); - Assert.Contains("No startup configured.", ex.Message); + Assert.Contains("No application configured.", ex.Message); } - [Fact] - public void DefaultApplicationNameWithUseStartupOfString() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void DefaultApplicationNameWithUseStartupOfString(IWebHostBuilder builder) { - var builder = new ConfigurationBuilder(); - using (var host = new WebHostBuilder() + using (var host = builder .UseServer(new TestServer()) .UseStartup(typeof(Startup).Assembly.GetName().Name) .Build()) @@ -651,40 +681,40 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void DefaultApplicationNameWithUseStartupOfT() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void DefaultApplicationNameWithUseStartupOfT(IWebHostBuilder builder) { - var builder = new ConfigurationBuilder(); - using (var host = new WebHostBuilder() + using (var host = builder .UseServer(new TestServer()) - .UseStartup() + .UseStartup() .Build()) { var hostingEnv = host.Services.GetService(); var hostingEnv2 = host.Services.GetService(); - Assert.Equal(typeof(StartupNoServices).Assembly.GetName().Name, hostingEnv.ApplicationName); - Assert.Equal(typeof(StartupNoServices).Assembly.GetName().Name, hostingEnv2.ApplicationName); + Assert.Equal(typeof(StartupNoServicesNoInterface).Assembly.GetName().Name, hostingEnv.ApplicationName); + Assert.Equal(typeof(StartupNoServicesNoInterface).Assembly.GetName().Name, hostingEnv2.ApplicationName); } } - [Fact] - public void DefaultApplicationNameWithUseStartupOfType() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void DefaultApplicationNameWithUseStartupOfType(IWebHostBuilder builder) { - var builder = new ConfigurationBuilder(); - var host = new WebHostBuilder() + var host = builder .UseServer(new TestServer()) - .UseStartup(typeof(StartupNoServices)) + .UseStartup(typeof(StartupNoServicesNoInterface)) .Build(); var hostingEnv = host.Services.GetService(); - Assert.Equal(typeof(StartupNoServices).Assembly.GetName().Name, hostingEnv.ApplicationName); + Assert.Equal(typeof(StartupNoServicesNoInterface).Assembly.GetName().Name, hostingEnv.ApplicationName); } - [Fact] - public void DefaultApplicationNameWithConfigure() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void DefaultApplicationNameWithConfigure(IWebHostBuilder builder) { - var builder = new ConfigurationBuilder(); - using (var host = new WebHostBuilder() + using (var host = builder .UseServer(new TestServer()) .Configure(app => { }) .Build()) @@ -696,10 +726,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Configure_SupportsNonStaticMethodDelegate() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void Configure_SupportsNonStaticMethodDelegate(IWebHostBuilder builder) { - using (var host = new WebHostBuilder() + using (var host = builder .UseServer(new TestServer()) .Configure(app => { }) .Build()) @@ -709,10 +740,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Configure_SupportsStaticMethodDelegate() + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public void Configure_SupportsStaticMethodDelegate(IWebHostBuilder builder) { - using (var host = new WebHostBuilder() + using (var host = builder .UseServer(new TestServer()) .Configure(StaticConfigureMethod) .Build()) @@ -736,11 +768,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Build_DoesNotOverrideILoggerFactorySetByConfigureServices() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_DoesNotOverrideILoggerFactorySetByConfigureServices(IWebHostBuilder builder) { var factory = new DisposableLoggerFactory(); - var builder = CreateWebHostBuilder(); var server = new TestServer(); using (var host = builder.UseServer(server) @@ -753,10 +785,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Build_RunsHostingStartupAssembliesIfSpecified() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_RunsHostingStartupAssembliesIfSpecified(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder() + builder = builder .CaptureStartupErrors(false) .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) .Configure(app => { }) @@ -768,10 +801,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Build_RunsHostingStartupRunsPrimaryAssemblyFirst() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_RunsHostingStartupRunsPrimaryAssemblyFirst(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder() + builder = builder .CaptureStartupErrors(false) .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) .Configure(app => { }) @@ -785,31 +819,28 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Build_RunsHostingStartupAssembliesBeforeApplication() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_RunsHostingStartupAssembliesBeforeApplication(IWebHostBuilder builder) { - var startup = new StartupVerifyServiceA(); var startupAssemblyName = typeof(WebHostBuilderTests).GetTypeInfo().Assembly.GetName().Name; - var builder = CreateWebHostBuilder() + builder = builder .CaptureStartupErrors(false) .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(WebHostBuilderTests).GetTypeInfo().Assembly.FullName) .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) - .ConfigureServices(services => - { - services.AddSingleton(startup); - }) + .UseStartup() .UseServer(new TestServer()); using (var host = builder.Build()) { host.Start(); + var startup = host.Services.GetRequiredService(); Assert.NotNull(startup.ServiceADescriptor); Assert.NotNull(startup.ServiceA); } } - [Fact] public async Task ExternalContainerInstanceCanBeUsedForEverything() { @@ -825,7 +856,7 @@ namespace Microsoft.AspNetCore.Hosting }); }); - var host = new WebHostBuilder() + var host = CreateWebHostBuilder() .UseStartup() .UseServer(new TestServer()) .ConfigureServices(services => @@ -849,9 +880,36 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void Build_HostingStartupAssemblyCanBeExcluded() + public void GenericWebHostThrowsWithIStartup() { - var builder = CreateWebHostBuilder() + var builder = new GenericWebHostBuilderWrapper(new HostBuilder()) + .UseStartup(); + + var exception = Assert.Throws(() => builder.Build()); + Assert.Equal("Microsoft.AspNetCore.Hosting.IStartup isn't supported", exception.Message); + } + + [Fact] + public void GenericWebHostThrowsOnBuild() + { + var exception = Assert.Throws(() => + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(builder => + { + builder.UseStartup(); + builder.Build(); + }); + }); + + Assert.Equal("Building this implementation of IWebHostBuilder is not supported.", exception.Message); + } + + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_HostingStartupAssemblyCanBeExcluded(IWebHostBuilder builder) + { + builder = builder .CaptureStartupErrors(false) .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) .UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) @@ -865,10 +923,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Build_ConfigureLoggingInHostingStartupWorks() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_ConfigureLoggingInHostingStartupWorks(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder() + builder = builder .CaptureStartupErrors(false) .Configure(app => { @@ -878,7 +937,7 @@ namespace Microsoft.AspNetCore.Hosting }) .UseServer(new TestServer()); - using (var host = (WebHost)builder.Build()) + using (var host = builder.Build()) { host.Start(); var sink = host.Services.GetRequiredService(); @@ -886,25 +945,27 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Build_ConfigureAppConfigurationInHostingStartupWorks() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_ConfigureAppConfigurationInHostingStartupWorks(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder() + builder = builder .CaptureStartupErrors(false) .Configure(app => { }) .UseServer(new TestServer()); - using (var host = (WebHost)builder.Build()) + using (var host = builder.Build()) { var configuration = host.Services.GetRequiredService(); Assert.Equal("value", configuration["testhostingstartup:config"]); } } - [Fact] - public void Build_DoesRunHostingStartupFromPrimaryAssemblyEvenIfNotSpecified() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_DoesRunHostingStartupFromPrimaryAssemblyEvenIfNotSpecified(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder() + builder = builder .Configure(app => { }) .UseServer(new TestServer()); @@ -914,10 +975,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Build_HostingStartupFromPrimaryAssemblyCanBeDisabled() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_HostingStartupFromPrimaryAssemblyCanBeDisabled(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder() + builder = builder .UseSetting(WebHostDefaults.PreventHostingStartupKey, "true") .Configure(app => { }) .UseServer(new TestServer()); @@ -928,10 +990,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void Build_DoesntThrowIfUnloadableAssemblyNameInHostingStartupAssemblies() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void Build_DoesntThrowIfUnloadableAssemblyNameInHostingStartupAssemblies(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder() + builder = builder .CaptureStartupErrors(false) .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "SomeBogusName") .Configure(app => { }) @@ -943,11 +1006,12 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public async Task Build_DoesNotThrowIfUnloadableAssemblyNameInHostingStartupAssembliesAndCaptureStartupErrorsTrue() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public async Task Build_DoesNotThrowIfUnloadableAssemblyNameInHostingStartupAssembliesAndCaptureStartupErrorsTrue(IWebHostBuilder builder) { var provider = new TestLoggerProvider(); - var builder = CreateWebHostBuilder() + builder = builder .ConfigureLogging((_, factory) => { factory.AddProvider(provider); @@ -965,10 +1029,11 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void StartupErrorsAreLoggedIfCaptureStartupErrorsIsTrue() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void StartupErrorsAreLoggedIfCaptureStartupErrorsIsTrue(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder() + builder = builder .CaptureStartupErrors(true) .Configure(app => { @@ -976,7 +1041,7 @@ namespace Microsoft.AspNetCore.Hosting }) .UseServer(new TestServer()); - using (var host = (WebHost)builder.Build()) + using (var host = builder.Build()) { host.Start(); var sink = host.Services.GetRequiredService(); @@ -984,12 +1049,13 @@ namespace Microsoft.AspNetCore.Hosting } } - [Fact] - public void StartupErrorsAreLoggedIfCaptureStartupErrorsIsFalse() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void StartupErrorsAreLoggedIfCaptureStartupErrorsIsFalse(IWebHostBuilder builder) { ITestSink testSink = null; - var builder = CreateWebHostBuilder() + builder = builder .CaptureStartupErrors(false) .Configure(app => { @@ -1017,21 +1083,52 @@ namespace Microsoft.AspNetCore.Hosting Assert.Throws(() => new HostingStartupAttribute(typeof(WebHostTests))); } - [Fact] - public void UseShutdownTimeoutConfiguresShutdownTimeout() + [Theory] + [MemberData(nameof(DefaultWebHostBuildersWithConfig))] + public void UseShutdownTimeoutConfiguresShutdownTimeout(IWebHostBuilder builder) { - var builder = CreateWebHostBuilder() + builder = builder .CaptureStartupErrors(false) .UseShutdownTimeout(TimeSpan.FromSeconds(102)) .Configure(app => { }) .UseServer(new TestServer()); - using (var host = (WebHost)builder.Build()) + using (var host = builder.Build()) { - Assert.Equal(TimeSpan.FromSeconds(102), host.Options.ShutdownTimeout); + var options = new WebHostOptions(host.Services.GetRequiredService()); + Assert.Equal(TimeSpan.FromSeconds(102), options.ShutdownTimeout); } } + [Theory] + [MemberData(nameof(DefaultWebHostBuilders))] + public async Task StartupFiltersDoNotRunIfNotApplicationConfigured(IWebHostBuilder builder) + { + var hostBuilder = builder + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .UseServer(new TestServer()); + + var exception = await Assert.ThrowsAsync(async () => + { + var host = hostBuilder.Build(); + var filter = (MyStartupFilter)host.Services.GetServices().FirstOrDefault(s => s is MyStartupFilter); + Assert.NotNull(filter); + try + { + await host.StartAsync(); + } + finally + { + Assert.False(filter.Executed); + } + }); + + Assert.Contains("No application configured.", exception.Message); + } + private static void StaticConfigureMethod(IApplicationBuilder app) { } private IWebHostBuilder CreateWebHostBuilder() @@ -1044,9 +1141,37 @@ namespace Microsoft.AspNetCore.Hosting var builder = new ConfigurationBuilder() .AddInMemoryCollection(vals); var config = builder.Build(); + return new WebHostBuilder().UseConfiguration(config); } + public static TheoryData DefaultWebHostBuilders => new TheoryData + { + new WebHostBuilder(), + new GenericWebHostBuilderWrapper(new HostBuilder()) + }; + + public static TheoryData DefaultWebHostBuildersWithConfig + { + get + { + var vals = new Dictionary + { + { "DetailedErrors", "true" }, + { "captureStartupErrors", "true" } + }; + + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + + return new TheoryData { + new WebHostBuilder().UseConfiguration(config), + new GenericWebHostBuilderWrapper(new HostBuilder()).UseConfiguration(config) + }; + } + } + private async Task AssertResponseContains(RequestDelegate app, string expectedText) { var httpContext = new DefaultHttpContext(); @@ -1057,6 +1182,17 @@ namespace Microsoft.AspNetCore.Hosting Assert.Contains(expectedText, bodyText); } + private class MyStartupFilter : IStartupFilter + { + public bool Executed { get; set; } + + public Action Configure(Action next) + { + Executed = true; + return next; + } + } + private class TestServer : IServer { IFeatureCollection IServer.Features { get; } @@ -1132,17 +1268,17 @@ namespace Microsoft.AspNetCore.Hosting } } - internal class StartupVerifyServiceA : IStartup + internal class StartupVerifyServiceA { internal ServiceA ServiceA { get; set; } internal ServiceDescriptor ServiceADescriptor { get; set; } - public IServiceProvider ConfigureServices(IServiceCollection services) + public void ConfigureServices(IServiceCollection services) { - ServiceADescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(ServiceA)); + services.AddSingleton(this); - return services.BuildServiceProvider(); + ServiceADescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(ServiceA)); } public void Configure(IApplicationBuilder app)