diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 0559d5234d..53312853da 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -19,25 +19,33 @@ + + + + + + + + @@ -45,16 +53,20 @@ + + + + diff --git a/src/Hosting/Hosting/src/GenericHost/GenericWebHostApplicationLifetime.cs b/src/Hosting/Hosting/src/GenericHost/GenericWebHostApplicationLifetime.cs new file mode 100644 index 0000000000..d95704154e --- /dev/null +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs b/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs new file mode 100644 index 0000000000..6a425883ea --- /dev/null +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/GenericHost/GenericWebHostServiceOptions.cs b/src/Hosting/Hosting/src/GenericHost/GenericWebHostServiceOptions.cs new file mode 100644 index 0000000000..715c43514e --- /dev/null +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/GenericHost/GenericWebHostedService.cs b/src/Hosting/Hosting/src/GenericHost/GenericWebHostedService.cs new file mode 100644 index 0000000000..8ec39059f0 --- /dev/null +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/GenericHost/HostingStartupWebHostBuilder.cs b/src/Hosting/Hosting/src/GenericHost/HostingStartupWebHostBuilder.cs new file mode 100644 index 0000000000..52eaec8179 --- /dev/null +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/GenericHost/ISupportsStartup.cs b/src/Hosting/Hosting/src/GenericHost/ISupportsStartup.cs new file mode 100644 index 0000000000..16322c8bea --- /dev/null +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/GenericHost/ISupportsUseDefaultServiceProvider.cs b/src/Hosting/Hosting/src/GenericHost/ISupportsUseDefaultServiceProvider.cs new file mode 100644 index 0000000000..bf9813cd6e --- /dev/null +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/GenericHostWebHostBuilderExtensions.cs b/src/Hosting/Hosting/src/GenericHostWebHostBuilderExtensions.cs new file mode 100644 index 0000000000..ea903ad7b5 --- /dev/null +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/Internal/ConfigureContainerBuilder.cs b/src/Hosting/Hosting/src/Internal/ConfigureContainerBuilder.cs index ed8d0fd06e..8791a918ab 100644 --- a/src/Hosting/Hosting/src/Internal/ConfigureContainerBuilder.cs +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/Internal/ConfigureServicesBuilder.cs b/src/Hosting/Hosting/src/Internal/ConfigureServicesBuilder.cs index 4206d0d62a..cf9a69327d 100644 --- a/src/Hosting/Hosting/src/Internal/ConfigureServicesBuilder.cs +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/Internal/StartupLoader.cs b/src/Hosting/Hosting/src/Internal/StartupLoader.cs index d7211d39d9..72dab81736 100644 --- a/src/Hosting/Hosting/src/Internal/StartupLoader.cs +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/Internal/WebHost.cs b/src/Hosting/Hosting/src/Internal/WebHost.cs index 3764427f63..0ff44958f1 100644 --- a/src/Hosting/Hosting/src/Internal/WebHost.cs +++ b/src/Hosting/Hosting/src/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/Hosting/Hosting/src/WebHostBuilder.cs b/src/Hosting/Hosting/src/WebHostBuilder.cs index 2238bd3b9b..8c104068d6 100644 --- a/src/Hosting/Hosting/src/WebHostBuilder.cs +++ b/src/Hosting/Hosting/src/WebHostBuilder.cs @@ -25,13 +25,13 @@ namespace Microsoft.AspNetCore.Hosting public class WebHostBuilder : IWebHostBuilder { private readonly HostingEnvironment _hostingEnvironment; - private readonly List> _configureServicesDelegates; + private Action _configureServices; private IConfiguration _config; private WebHostOptions _options; private WebHostBuilderContext _context; private bool _webHostBuilt; - private List> _configureAppConfigurationBuilderDelegates; + private Action _configureAppConfigurationBuilder; /// /// Initializes a new instance of the class. @@ -39,8 +39,6 @@ namespace Microsoft.AspNetCore.Hosting public WebHostBuilder() { _hostingEnvironment = new HostingEnvironment(); - _configureServicesDelegates = new List>(); - _configureAppConfigurationBuilderDelegates = new List>(); _config = new ConfigurationBuilder() .AddEnvironmentVariables(prefix: "ASPNETCORE_") @@ -111,12 +109,7 @@ namespace Microsoft.AspNetCore.Hosting /// The . public IWebHostBuilder ConfigureServices(Action configureServices) { - if (configureServices == null) - { - throw new ArgumentNullException(nameof(configureServices)); - } - - _configureServicesDelegates.Add(configureServices); + _configureServices += configureServices; return this; } @@ -131,12 +124,7 @@ namespace Microsoft.AspNetCore.Hosting /// public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate) { - if (configureDelegate == null) - { - throw new ArgumentNullException(nameof(configureDelegate)); - } - - _configureAppConfigurationBuilderDelegates.Add(configureDelegate); + _configureAppConfigurationBuilder += configureDelegate; return this; } @@ -273,10 +261,7 @@ namespace Microsoft.AspNetCore.Hosting .SetBasePath(_hostingEnvironment.ContentRootPath) .AddConfiguration(_config); - foreach (var configureAppConfiguration in _configureAppConfigurationBuilderDelegates) - { - configureAppConfiguration(_context, builder); - } + _configureAppConfigurationBuilder?.Invoke(_context, builder); var configuration = builder.Build(); services.AddSingleton(configuration); @@ -330,10 +315,7 @@ namespace Microsoft.AspNetCore.Hosting } } - foreach (var configureServices in _configureServicesDelegates) - { - configureServices(_context, services); - } + _configureServices?.Invoke(_context, services); return services; } diff --git a/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs b/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs index 09c7e6d96b..63dd3257b8 100644 --- a/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs +++ b/src/Hosting/Hosting/src/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/src/Hosting/Hosting/test/Fakes/GenericWebHost.cs b/src/Hosting/Hosting/test/Fakes/GenericWebHost.cs new file mode 100644 index 0000000000..d61ce147a7 --- /dev/null +++ b/src/Hosting/Hosting/test/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/src/Hosting/Hosting/test/Fakes/GenericWebHostBuilderWrapper.cs b/src/Hosting/Hosting/test/Fakes/GenericWebHostBuilderWrapper.cs new file mode 100644 index 0000000000..3ff3aeef51 --- /dev/null +++ b/src/Hosting/Hosting/test/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/src/Hosting/Hosting/test/Fakes/StartupNoServicesNoInterface.cs b/src/Hosting/Hosting/test/Fakes/StartupNoServicesNoInterface.cs new file mode 100644 index 0000000000..d97cee737a --- /dev/null +++ b/src/Hosting/Hosting/test/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/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj b/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj index 151a66777a..5a06f47a90 100644 --- a/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj +++ b/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Hosting/Hosting/test/WebHostBuilderTests.cs b/src/Hosting/Hosting/test/WebHostBuilderTests.cs index c1244e5c8f..876959ddb4 100644 --- a/src/Hosting/Hosting/test/WebHostBuilderTests.cs +++ b/src/Hosting/Hosting/test/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) diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs b/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs index 56715d3c08..4fb0bb57ef 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting public const string NetCoreApp20 = "netcoreapp2.0"; public const string NetCoreApp21 = "netcoreapp2.1"; public const string NetCoreApp22 = "netcoreapp2.2"; + public const string NetCoreApp30 = "netcoreapp3.0"; public static bool Matches(string tfm1, string tfm2) { diff --git a/src/Hosting/TestHost/src/ClientHandler.cs b/src/Hosting/TestHost/src/ClientHandler.cs index 2109809d1a..5471e23d19 100644 --- a/src/Hosting/TestHost/src/ClientHandler.cs +++ b/src/Hosting/TestHost/src/ClientHandler.cs @@ -72,10 +72,20 @@ namespace Microsoft.AspNetCore.TestHost req.Method = request.Method.ToString(); req.Scheme = request.RequestUri.Scheme; - req.Host = HostString.FromUriComponent(request.RequestUri); - if (request.RequestUri.IsDefaultPort) + + foreach (var header in request.Headers) { - req.Host = new HostString(req.Host.Host); + req.Headers.Append(header.Key, header.Value.ToArray()); + } + + if (req.Host == null || !req.Host.HasValue) + { + // If Host wasn't explicitly set as a header, let's infer it from the Uri + req.Host = HostString.FromUriComponent(request.RequestUri); + if (request.RequestUri.IsDefaultPort) + { + req.Host = new HostString(req.Host.Host); + } } req.Path = PathString.FromUriComponent(request.RequestUri); @@ -87,10 +97,6 @@ namespace Microsoft.AspNetCore.TestHost } req.QueryString = QueryString.FromUriComponent(request.RequestUri); - foreach (var header in request.Headers) - { - req.Headers.Append(header.Key, header.Value.ToArray()); - } if (requestContent != null) { foreach (var header in requestContent.Headers) diff --git a/src/Hosting/TestHost/src/HostBuilderTestServerExtensions.cs b/src/Hosting/TestHost/src/HostBuilderTestServerExtensions.cs new file mode 100644 index 0000000000..e52f3d6ab0 --- /dev/null +++ b/src/Hosting/TestHost/src/HostBuilderTestServerExtensions.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.Net.Http; +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.TestHost +{ + public static class HostBuilderTestServerExtensions + { + /// + /// Retrieves the TestServer from the host services. + /// + /// + /// + public static TestServer GetTestServer(this IHost host) + { + return (TestServer)host.Services.GetRequiredService(); + } + + /// + /// Retrieves the test client from the TestServer in the host services. + /// + /// + /// + public static HttpClient GetTestClient(this IHost host) + { + return host.GetTestServer().CreateClient(); + } + } +} diff --git a/src/Hosting/TestHost/src/HttpContextBuilder.cs b/src/Hosting/TestHost/src/HttpContextBuilder.cs index 6886b1aac4..c576628b65 100644 --- a/src/Hosting/TestHost/src/HttpContextBuilder.cs +++ b/src/Hosting/TestHost/src/HttpContextBuilder.cs @@ -109,7 +109,16 @@ namespace Microsoft.AspNetCore.TestHost if (!_responseFeature.HasStarted) { // Sets HasStarted - await _responseFeature.FireOnSendingHeadersAsync(); + try + { + await _responseFeature.FireOnSendingHeadersAsync(); + } + catch (Exception ex) + { + Abort(ex); + return; + } + // Copy the feature collection so we're not multi-threading on the same collection. var newFeatures = new FeatureCollection(); foreach (var pair in _httpContext.Features) diff --git a/src/Hosting/TestHost/src/TestServer.cs b/src/Hosting/TestHost/src/TestServer.cs index 398a575d9d..40a3e63f05 100644 --- a/src/Hosting/TestHost/src/TestServer.cs +++ b/src/Hosting/TestHost/src/TestServer.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -15,28 +16,48 @@ namespace Microsoft.AspNetCore.TestHost { public class TestServer : IServer { - private const string ServerName = nameof(TestServer); private IWebHost _hostInstance; private bool _disposed = false; private IHttpApplication _application; + /// + /// For use with IHostBuilder or IWebHostBuilder. + /// + public TestServer() + : this(new FeatureCollection()) + { + } + + /// + /// For use with IHostBuilder or IWebHostBuilder. + /// + /// + public TestServer(IFeatureCollection featureCollection) + { + Features = featureCollection ?? throw new ArgumentNullException(nameof(featureCollection)); + } + + /// + /// For use with IWebHostBuilder. + /// + /// public TestServer(IWebHostBuilder builder) : this(builder, new FeatureCollection()) { } + /// + /// For use with IWebHostBuilder. + /// + /// + /// public TestServer(IWebHostBuilder builder, IFeatureCollection featureCollection) + : this(featureCollection) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } - if (featureCollection == null) - { - throw new ArgumentNullException(nameof(featureCollection)); - } - - Features = featureCollection; var host = builder.UseServer(this).Build(); host.StartAsync().GetAwaiter().GetResult(); @@ -49,16 +70,22 @@ namespace Microsoft.AspNetCore.TestHost { get { - return _hostInstance; + return _hostInstance + ?? throw new InvalidOperationException("The TestServer constructor was not called with a IWebHostBuilder so IWebHost is not available."); } } public IFeatureCollection Features { get; } + private IHttpApplication Application + { + get => _application ?? throw new InvalidOperationException("The server has not been started or no web application was configured."); + } + public HttpMessageHandler CreateHandler() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new ClientHandler(pathBase, _application); + return new ClientHandler(pathBase, Application); } public HttpClient CreateClient() @@ -69,7 +96,7 @@ namespace Microsoft.AspNetCore.TestHost public WebSocketClient CreateWebSocketClient() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new WebSocketClient(pathBase, _application); + return new WebSocketClient(pathBase, Application); } /// @@ -93,7 +120,7 @@ namespace Microsoft.AspNetCore.TestHost throw new ArgumentNullException(nameof(configureContext)); } - var builder = new HttpContextBuilder(_application); + var builder = new HttpContextBuilder(Application); builder.Configure(context => { var request = context.Request; diff --git a/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs index cccf19cdd5..308ac86cd4 100644 --- a/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs +++ b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs @@ -6,12 +6,21 @@ using System.IO; using System.Linq; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Hosting.Server; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.TestHost { public static class WebHostBuilderExtensions { + public static IWebHostBuilder UseTestServer(this IWebHostBuilder builder) + { + return builder.ConfigureServices(services => + { + services.AddSingleton(); + }); + } + public static IWebHostBuilder ConfigureTestServices(this IWebHostBuilder webHostBuilder, Action servicesConfiguration) { if (webHostBuilder == null) diff --git a/src/Hosting/TestHost/test/ClientHandlerTests.cs b/src/Hosting/TestHost/test/ClientHandlerTests.cs index e90a79e97b..7187f493dc 100644 --- a/src/Hosting/TestHost/test/ClientHandlerTests.cs +++ b/src/Hosting/TestHost/test/ClientHandlerTests.cs @@ -272,6 +272,63 @@ namespace Microsoft.AspNetCore.TestHost Assert.IsType(ex.GetBaseException()); } + [Fact] + public Task ExceptionFromOnStartingFirstWriteIsReported() + { + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + { + context.Response.OnStarting(() => + { + throw new InvalidOperationException(new string('a', 1024 * 32)); + }); + return context.Response.WriteAsync("Hello World"); + })); + var httpClient = new HttpClient(handler); + return Assert.ThrowsAsync(() => httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead)); + } + + [Fact] + public Task ExceptionFromOnStartingWithNoWriteIsReported() + { + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + { + context.Response.OnStarting(() => + { + throw new InvalidOperationException(new string('a', 1024 * 32)); + }); + return Task.CompletedTask; + })); + var httpClient = new HttpClient(handler); + return Assert.ThrowsAsync(() => httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead)); + } + + [Fact] + public Task ExceptionFromOnStartingWithErrorHandlerIsReported() + { + var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => + { + context.Response.OnStarting(() => + { + throw new InvalidOperationException(new string('a', 1024 * 32)); + }); + try + { + await context.Response.WriteAsync("Hello World"); + } + catch (Exception ex) + { + // This is no longer the first write, so it doesn't trigger OnStarting again. + // The exception is large enough that it fills the pipe and stalls. + await context.Response.WriteAsync(ex.ToString()); + } + })); + var httpClient = new HttpClient(handler); + return Assert.ThrowsAsync(() => httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead)); + } + private class DummyApplication : IHttpApplication { RequestDelegate _application; diff --git a/src/Hosting/TestHost/test/Microsoft.AspNetCore.TestHost.Tests.csproj b/src/Hosting/TestHost/test/Microsoft.AspNetCore.TestHost.Tests.csproj index 51041799c4..4576a08eec 100644 --- a/src/Hosting/TestHost/test/Microsoft.AspNetCore.TestHost.Tests.csproj +++ b/src/Hosting/TestHost/test/Microsoft.AspNetCore.TestHost.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Hosting/TestHost/test/TestServerTests.cs b/src/Hosting/TestHost/test/TestServerTests.cs index b1eee1fd94..cc789962c7 100644 --- a/src/Hosting/TestHost/test/TestServerTests.cs +++ b/src/Hosting/TestHost/test/TestServerTests.cs @@ -15,13 +15,65 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DiagnosticAdapter; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.TestHost { public class TestServerTests { + [Fact] + public async Task GenericRawCreate() + { + var server = new TestServer(); + var host = new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseServer(server) + .Configure(app => { }); + }) + .Build(); + await host.StartAsync(); + + var response = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GenericCreateAndStartHost_GetTestServer() + { + var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseTestServer() + .Configure(app => { }); + }) + .StartAsync(); + + var response = await host.GetTestServer().CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GenericCreateAndStartHost_GetTestClient() + { + var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseTestServer() + .Configure(app => { }); + }) + .StartAsync(); + + var response = await host.GetTestClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + [Fact] public void CreateWithDelegate() { @@ -30,6 +82,17 @@ namespace Microsoft.AspNetCore.TestHost new TestServer(new WebHostBuilder().Configure(app => { })); } + [Fact] + public void CreateWithDelegate_DI() + { + var builder = new WebHostBuilder() + .Configure(app => { }) + .UseTestServer(); + + var host = builder.Build(); + host.Start(); + } + [Fact] public void DoesNotCaptureStartupErrorsByDefault() { @@ -576,6 +639,29 @@ namespace Microsoft.AspNetCore.TestHost Assert.NotNull(listener.UnhandledException?.Exception); } + [Theory] + [InlineData("http://localhost:12345")] + [InlineData("http://localhost:12345/")] + [InlineData("http://localhost:12345/hellohellohello")] + [InlineData("/isthereanybodyinthere?")] + public async Task ManuallySetHostWinsOverInferredHostFromRequestUri(string uri) + { + RequestDelegate appDelegate = ctx => + ctx.Response.WriteAsync(ctx.Request.Headers[HeaderNames.Host]); + + var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, uri); + request.Headers.Host = "otherhost:5678"; + + var response = await client.SendAsync(request); + var responseBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("otherhost:5678", responseBody); + } + public class TestDiagnosticListener { public class OnBeginRequestEventData diff --git a/src/Hosting/samples/GenericWebHost/Program.cs b/src/Hosting/samples/GenericWebHost/Program.cs index 4879031f56..653541ef7a 100644 --- a/src/Hosting/samples/GenericWebHost/Program.cs +++ b/src/Hosting/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(); } }