diff --git a/Hosting.sln b/Hosting.sln index 5fd76b63ed..09f0ae3c38 100644 --- a/Hosting.sln +++ b/Hosting.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26228.4 +VisualStudioVersion = 15.0.26418.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E0497F39-AFFB-4819-A116-E39E361915AB}" EndProject @@ -31,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Hostin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Hosting.TestSites", "test\Microsoft.AspNetCore.Hosting.TestSites\Microsoft.AspNetCore.Hosting.TestSites.csproj", "{542D4600-B232-4B17-A08C-E31EBFA0D74E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestStartupAssembly1", "test\TestStartupAssembly1\TestStartupAssembly1.csproj", "{39D3B138-37DB-4D03-A5A0-3F2B02EFC671}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -165,6 +166,18 @@ Global {542D4600-B232-4B17-A08C-E31EBFA0D74E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {542D4600-B232-4B17-A08C-E31EBFA0D74E}.Release|x86.ActiveCfg = Release|Any CPU {542D4600-B232-4B17-A08C-E31EBFA0D74E}.Release|x86.Build.0 = Release|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Debug|x86.ActiveCfg = Debug|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Debug|x86.Build.0 = Debug|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Release|Any CPU.Build.0 = Release|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Release|x86.ActiveCfg = Release|Any CPU + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -181,5 +194,6 @@ Global {03148731-EA95-40A2-BAE8-A12315EA1748} = {E0497F39-AFFB-4819-A116-E39E361915AB} {FC578F4E-171C-4F82-B301-3ABF6318D082} = {FEB39027-9158-4DE2-997F-7ADAEF8188D0} {542D4600-B232-4B17-A08C-E31EBFA0D74E} = {FEB39027-9158-4DE2-997F-7ADAEF8188D0} + {39D3B138-37DB-4D03-A5A0-3F2B02EFC671} = {FEB39027-9158-4DE2-997F-7ADAEF8188D0} EndGlobalSection EndGlobal diff --git a/build/repo.props b/build/repo.props index 9bbb94d668..2e289dd92d 100644 --- a/build/repo.props +++ b/build/repo.props @@ -1,5 +1,6 @@ + diff --git a/samples/SampleStartups/FakeServer.cs b/samples/SampleStartups/FakeServer.cs new file mode 100644 index 0000000000..a43790c352 --- /dev/null +++ b/samples/SampleStartups/FakeServer.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace SampleStartups +{ + // We can't reference real servers in this sample without creating a circular repo dependency. + // This fake server lets us at least run the code. + public class FakeServer : IServer + { + public IFeatureCollection Features => new FeatureCollection(); + + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public void Dispose() + { + } + } + + public static class FakeServerWebHostBuliderExtensions + { + public static IWebHostBuilder UseFakeServer(this IWebHostBuilder builder) + { + return builder.ConfigureServices(services => services.AddSingleton()); + } + } +} diff --git a/samples/SampleStartups/SampleStartups.csproj b/samples/SampleStartups/SampleStartups.csproj index 3e29e7642d..23f037440d 100644 --- a/samples/SampleStartups/SampleStartups.csproj +++ b/samples/SampleStartups/SampleStartups.csproj @@ -4,6 +4,8 @@ net46;netcoreapp2.0 + SampleStartups.StartupInjection + exe diff --git a/samples/SampleStartups/StartupBlockingOnStart.cs b/samples/SampleStartups/StartupBlockingOnStart.cs index 1bdecf438a..c46010e18b 100644 --- a/samples/SampleStartups/StartupBlockingOnStart.cs +++ b/samples/SampleStartups/StartupBlockingOnStart.cs @@ -34,6 +34,7 @@ namespace SampleStartups var host = new WebHostBuilder() .UseConfiguration(config) + .UseFakeServer() .UseStartup() .Build(); diff --git a/samples/SampleStartups/StartupConfigureAddresses.cs b/samples/SampleStartups/StartupConfigureAddresses.cs index 3fe6c20d0b..8413c47c90 100644 --- a/samples/SampleStartups/StartupConfigureAddresses.cs +++ b/samples/SampleStartups/StartupConfigureAddresses.cs @@ -25,6 +25,7 @@ namespace SampleStartups var host = new WebHostBuilder() .UseConfiguration(config) + .UseFakeServer() .UseStartup() .UseUrls("http://localhost:5000", "http://localhost:5001") .Build(); diff --git a/samples/SampleStartups/StartupExternallyControlled.cs b/samples/SampleStartups/StartupExternallyControlled.cs index 218624bd65..4fa9e55b87 100644 --- a/samples/SampleStartups/StartupExternallyControlled.cs +++ b/samples/SampleStartups/StartupExternallyControlled.cs @@ -32,6 +32,7 @@ namespace SampleStartups { _host = new WebHostBuilder() //.UseKestrel() + .UseFakeServer() .UseStartup() .Start(_urls.ToArray()); } diff --git a/samples/SampleStartups/StartupFullControl.cs b/samples/SampleStartups/StartupFullControl.cs index a9254e482b..0f0207564a 100644 --- a/samples/SampleStartups/StartupFullControl.cs +++ b/samples/SampleStartups/StartupFullControl.cs @@ -22,7 +22,8 @@ namespace SampleStartups var host = new WebHostBuilder() .UseConfiguration(config) // Default set of configurations to use, may be subsequently overridden - //.UseKestrel() + //.UseKestrel() + .UseFakeServer() .UseContentRoot(Directory.GetCurrentDirectory()) // Override the content root with the current directory .UseUrls("http://*:1000", "https://*:902") .UseEnvironment("Development") diff --git a/samples/SampleStartups/StartupHelloWorld.cs b/samples/SampleStartups/StartupHelloWorld.cs index a2f806f661..88a77cfc6c 100644 --- a/samples/SampleStartups/StartupHelloWorld.cs +++ b/samples/SampleStartups/StartupHelloWorld.cs @@ -21,7 +21,8 @@ namespace SampleStartups public static void Main(string[] args) { var host = new WebHostBuilder() - //.UseKestrel() + //.UseKestrel() + .UseFakeServer() .UseStartup() .Build(); diff --git a/samples/SampleStartups/StartupInjection.cs b/samples/SampleStartups/StartupInjection.cs new file mode 100644 index 0000000000..381d621a10 --- /dev/null +++ b/samples/SampleStartups/StartupInjection.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +// HostingStartup's in the primary assembly are run automatically. +[assembly: HostingStartup(typeof(SampleStartups.StartupInjection))] + +namespace SampleStartups +{ + public class StartupInjection : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.UseStartup(); + } + + // Entry point for the application. + public static void Main(string[] args) + { + var host = new WebHostBuilder() + //.UseKestrel() + .UseFakeServer() + // Each of these three sets ApplicationName to the current assembly, which is needed in order to + // scan the assembly for HostingStartupAttributes. + // .UseSetting(WebHostDefaults.ApplicationKey, "SampleStartups") + // .Configure(_ => { }) + .UseStartup() + .Build(); + + host.Run(); + } + } + + public class NormalStartup + { + public void ConfigureServices(IServiceCollection services) + { + Console.WriteLine("NormalStartup.ConfigureServices"); + } + + public void Configure(IApplicationBuilder app) + { + Console.WriteLine("NormalStartup.Configure"); + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + } + } + + public class InjectedStartup + { + public void ConfigureServices(IServiceCollection services) + { + Console.WriteLine("InjectedStartup.ConfigureServices"); + } + + public void Configure(IApplicationBuilder app) + { + Console.WriteLine("InjectedStartup.Configure"); + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + } + } +} diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostDefaults.cs b/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostDefaults.cs index a7762b113e..cf0edb62b8 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostDefaults.cs +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostDefaults.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Hosting public static readonly string ServerUrlsKey = "urls"; public static readonly string ContentRootKey = "contentRoot"; public static readonly string PreferHostingUrlsKey = "preferHostingUrls"; + public static readonly string PreventHostingStartupKey = "preventHostingStartup"; public static readonly string ShutdownTimeoutKey = "shutdownTimeoutSeconds"; } diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/WebHostOptions.cs b/src/Microsoft.AspNetCore.Hosting/Internal/WebHostOptions.cs index 2102706069..0a5ca14cf9 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/WebHostOptions.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/WebHostOptions.cs @@ -26,7 +26,10 @@ namespace Microsoft.AspNetCore.Hosting.Internal Environment = configuration[WebHostDefaults.EnvironmentKey]; WebRoot = configuration[WebHostDefaults.WebRootKey]; ContentRootPath = configuration[WebHostDefaults.ContentRootKey]; - HostingStartupAssemblies = configuration[WebHostDefaults.HostingStartupAssembliesKey]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0]; + PreventHostingStartup = ParseBool(configuration, WebHostDefaults.PreventHostingStartupKey); + // Search the primary assembly and configured assemblies. + HostingStartupAssemblies = $"{ApplicationName};{configuration[WebHostDefaults.HostingStartupAssembliesKey]}" + .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0]; PreferHostingUrls = ParseBool(configuration, WebHostDefaults.PreferHostingUrlsKey); var timeout = configuration[WebHostDefaults.ShutdownTimeoutKey]; @@ -39,6 +42,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal public string ApplicationName { get; set; } + public bool PreventHostingStartup { get; set; } + public IReadOnlyList HostingStartupAssemblies { get; set; } public bool DetailedErrors { get; set; } diff --git a/src/Microsoft.AspNetCore.Hosting/WebHostBuilder.cs b/src/Microsoft.AspNetCore.Hosting/WebHostBuilder.cs index 336cec3f40..5ae258f924 100644 --- a/src/Microsoft.AspNetCore.Hosting/WebHostBuilder.cs +++ b/src/Microsoft.AspNetCore.Hosting/WebHostBuilder.cs @@ -302,36 +302,39 @@ namespace Microsoft.AspNetCore.Hosting services.AddSingleton(loggerFactory); _context.LoggerFactory = loggerFactory; - var exceptions = new List(); - - // Execute the hosting startup assemblies - foreach (var assemblyName in _options.HostingStartupAssemblies) + if (!_options.PreventHostingStartup) { - try - { - var assembly = Assembly.Load(new AssemblyName(assemblyName)); + var exceptions = new List(); - foreach (var attribute in assembly.GetCustomAttributes()) + // Execute the hosting startup assemblies + foreach (var assemblyName in _options.HostingStartupAssemblies) + { + try { - var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType); - hostingStartup.Configure(this); + var assembly = Assembly.Load(new AssemblyName(assemblyName)); + + foreach (var attribute in assembly.GetCustomAttributes()) + { + var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType); + hostingStartup.Configure(this); + } + } + 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)); } } - 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); - - // Throw directly if we're not capturing startup errors - if (!_options.CaptureStartupErrors) + if (exceptions.Count > 0) { - throw hostingStartupErrors; + hostingStartupErrors = new AggregateException(exceptions); + + // Throw directly if we're not capturing startup errors + if (!_options.CaptureStartupErrors) + { + throw hostingStartupErrors; + } } } 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 37912abcaa..7de02b4777 100644 --- a/test/Microsoft.AspNetCore.Hosting.Tests/Microsoft.AspNetCore.Hosting.Tests.csproj +++ b/test/Microsoft.AspNetCore.Hosting.Tests/Microsoft.AspNetCore.Hosting.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs index c2eb9c4364..6993805d92 100644 --- a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs +++ b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostBuilderTests.cs @@ -833,13 +833,30 @@ namespace Microsoft.AspNetCore.Hosting { var builder = CreateWebHostBuilder() .CaptureStartupErrors(false) - .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(WebHostBuilderTests).GetTypeInfo().Assembly.FullName) + .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) .Configure(app => { }) .UseServer(new TestServer()); using (var host = builder.Build()) { - Assert.Equal("1", builder.GetSetting("testhostingstartup")); + Assert.Equal("1", builder.GetSetting("testhostingstartup1")); + } + } + + [Fact] + public void Build_RunsHostingStartupRunsPrimaryAssemblyFirst() + { + var builder = CreateWebHostBuilder() + .CaptureStartupErrors(false) + .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(TestStartupAssembly1.TestHostingStartup1).GetTypeInfo().Assembly.FullName) + .Configure(app => { }) + .UseServer(new TestServer()); + + using (var host = builder.Build()) + { + Assert.Equal("0", builder.GetSetting("testhostingstartup")); + Assert.Equal("1", builder.GetSetting("testhostingstartup1")); + Assert.Equal("01", builder.GetSetting("testhostingstartup_chain")); } } @@ -871,7 +888,6 @@ namespace Microsoft.AspNetCore.Hosting { var builder = CreateWebHostBuilder() .CaptureStartupErrors(false) - .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(WebHostBuilderTests).GetTypeInfo().Assembly.FullName) .Configure(app => { var loggerFactory = app.ApplicationServices.GetService(); @@ -888,12 +904,26 @@ namespace Microsoft.AspNetCore.Hosting } [Fact] - public void Build_DoesNotRunHostingStartupAssembliesDoNotRunIfNotSpecified() + public void Build_DoesRunHostingStartupFromPrimaryAssemblyEvenIfNotSpecified() { var builder = CreateWebHostBuilder() .Configure(app => { }) .UseServer(new TestServer()); + using (builder.Build()) + { + Assert.Equal("0", builder.GetSetting("testhostingstartup")); + } + } + + [Fact] + public void Build_HostingStartupFromPrimaryAssemblyCanBeDisabled() + { + var builder = CreateWebHostBuilder() + .UseSetting(WebHostDefaults.PreventHostingStartupKey, "true") + .Configure(app => { }) + .UseServer(new TestServer()); + using (builder.Build()) { Assert.Null(builder.GetSetting("testhostingstartup")); @@ -1027,7 +1057,8 @@ namespace Microsoft.AspNetCore.Hosting public void Configure(IWebHostBuilder builder) { var loggerProvider = new TestLoggerProvider(); - builder.UseSetting("testhostingstartup", "1") + builder.UseSetting("testhostingstartup", "0") + .UseSetting("testhostingstartup_chain", builder.GetSetting("testhostingstartup_chain") + "0") .ConfigureServices(services => services.AddSingleton()) .ConfigureServices(services => services.AddSingleton(loggerProvider.Sink)) .ConfigureLogging(lf => lf.AddProvider(loggerProvider)); diff --git a/test/TestStartupAssembly1/TestHostingStartup1.cs b/test/TestStartupAssembly1/TestHostingStartup1.cs new file mode 100644 index 0000000000..e8519c83c3 --- /dev/null +++ b/test/TestStartupAssembly1/TestHostingStartup1.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; + +[assembly: HostingStartup(typeof(TestStartupAssembly1.TestHostingStartup1))] + +namespace TestStartupAssembly1 +{ + public class TestHostingStartup1 : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.UseSetting("testhostingstartup1", "1"); + builder.UseSetting("testhostingstartup_chain", builder.GetSetting("testhostingstartup_chain") + "1"); + } + } +} diff --git a/test/TestStartupAssembly1/TestStartupAssembly1.csproj b/test/TestStartupAssembly1/TestStartupAssembly1.csproj new file mode 100644 index 0000000000..f2b53ef9f3 --- /dev/null +++ b/test/TestStartupAssembly1/TestStartupAssembly1.csproj @@ -0,0 +1,13 @@ + + + + + + netcoreapp2.0;net46 + + + + + + + \ No newline at end of file