diff --git a/Hosting.sln b/Hosting.sln index f9325585a1..2128f4ea5f 100644 --- a/Hosting.sln +++ b/Hosting.sln @@ -55,6 +55,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.targets = Directory.Build.targets EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericHostSample", "samples\GenericHostSample\GenericHostSample.csproj", "{8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Hosting", "src\Microsoft.Extensions.Hosting\Microsoft.Extensions.Hosting.csproj", "{1DA77D55-5DB9-4426-87DC-758579335944}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericWebHost", "samples\GenericWebHost\GenericWebHost.csproj", "{FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Hosting.Tests", "test\Microsoft.Extensions.Hosting.Tests\Microsoft.Extensions.Hosting.Tests.csproj", "{45E296BB-7628-49AF-B5A5-04CD9A89CAD3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -237,6 +245,54 @@ Global {F894D8C5-B760-4734-AD31-3CA6FC557CCF}.Release|Mixed Platforms.Build.0 = Release|Any CPU {F894D8C5-B760-4734-AD31-3CA6FC557CCF}.Release|x86.ActiveCfg = Release|Any CPU {F894D8C5-B760-4734-AD31-3CA6FC557CCF}.Release|x86.Build.0 = Release|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Debug|x86.Build.0 = Debug|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Release|Any CPU.Build.0 = Release|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Release|x86.ActiveCfg = Release|Any CPU + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE}.Release|x86.Build.0 = Release|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Debug|x86.ActiveCfg = Debug|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Debug|x86.Build.0 = Debug|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Release|Any CPU.Build.0 = Release|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Release|x86.ActiveCfg = Release|Any CPU + {1DA77D55-5DB9-4426-87DC-758579335944}.Release|x86.Build.0 = Release|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Debug|x86.ActiveCfg = Debug|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Debug|x86.Build.0 = Debug|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Release|Any CPU.Build.0 = Release|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Release|x86.ActiveCfg = Release|Any CPU + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF}.Release|x86.Build.0 = Release|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Debug|x86.Build.0 = Debug|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Release|Any CPU.Build.0 = Release|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Release|x86.ActiveCfg = Release|Any CPU + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -258,6 +314,10 @@ Global {EDFF02F0-A8A4-4EB1-A179-94D7500FB266} = {FA7D2012-C1B4-4AF7-9ADD-381B2004EA16} {58194285-5891-464A-A96B-0FE043029E8A} = {FA7D2012-C1B4-4AF7-9ADD-381B2004EA16} {F894D8C5-B760-4734-AD31-3CA6FC557CCF} = {FA7D2012-C1B4-4AF7-9ADD-381B2004EA16} + {8529E5F7-059F-4A06-AD6E-ECDF4F4838FE} = {9C7520A0-F2EB-411C-8BB2-80B39C937217} + {1DA77D55-5DB9-4426-87DC-758579335944} = {E0497F39-AFFB-4819-A116-E39E361915AB} + {FCDD1C82-623C-4779-8A9C-B0D827CEA8BF} = {9C7520A0-F2EB-411C-8BB2-80B39C937217} + {45E296BB-7628-49AF-B5A5-04CD9A89CAD3} = {FEB39027-9158-4DE2-997F-7ADAEF8188D0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AABD536D-E05F-409B-A716-535E0C478076} diff --git a/samples/GenericHostSample/GenericHostSample.csproj b/samples/GenericHostSample/GenericHostSample.csproj new file mode 100644 index 0000000000..2f60b84b2e --- /dev/null +++ b/samples/GenericHostSample/GenericHostSample.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp2.0;net461 + GenericHostSample.ProgramHelloWorld + Exe + latest + + + + + + + + + + + + + + + + + diff --git a/samples/GenericHostSample/MyContainer.cs b/samples/GenericHostSample/MyContainer.cs new file mode 100644 index 0000000000..11556a845e --- /dev/null +++ b/samples/GenericHostSample/MyContainer.cs @@ -0,0 +1,6 @@ +namespace GenericHostSample +{ + internal class MyContainer + { + } +} \ No newline at end of file diff --git a/samples/GenericHostSample/MyContainerFactory.cs b/samples/GenericHostSample/MyContainerFactory.cs new file mode 100644 index 0000000000..d4fd399d77 --- /dev/null +++ b/samples/GenericHostSample/MyContainerFactory.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace GenericHostSample +{ + internal class MyContainerFactory : IServiceProviderFactory + { + public MyContainer CreateBuilder(IServiceCollection services) + { + return new MyContainer(); + } + + public IServiceProvider CreateServiceProvider(MyContainer containerBuilder) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/samples/GenericHostSample/MyServiceA.cs b/samples/GenericHostSample/MyServiceA.cs new file mode 100644 index 0000000000..93be796bbb --- /dev/null +++ b/samples/GenericHostSample/MyServiceA.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace GenericHostSample +{ + public class MyServiceA : IHostedService + { + private bool _stopping; + private Task _backgroundTask; + + public Task StartAsync(CancellationToken cancellationToken) + { + Console.WriteLine("MyServiceA is starting."); + _backgroundTask = BackgroundTask(); + return Task.CompletedTask; + } + + private async Task BackgroundTask() + { + while (!_stopping) + { + await Task.Delay(TimeSpan.FromSeconds(5)); + Console.WriteLine("MyServiceA is doing background work."); + } + + Console.WriteLine("MyServiceA background task is stopping."); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + Console.WriteLine("MyServiceA is stopping."); + _stopping = true; + if (_backgroundTask != null) + { + // TODO: cancellation + await _backgroundTask; + } + } + } +} diff --git a/samples/GenericHostSample/MyServiceB.cs b/samples/GenericHostSample/MyServiceB.cs new file mode 100644 index 0000000000..a143758b90 --- /dev/null +++ b/samples/GenericHostSample/MyServiceB.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace GenericHostSample +{ + public class MyServiceB : IHostedService + { + private bool _stopping; + private Task _backgroundTask; + + public Task StartAsync(CancellationToken cancellationToken) + { + Console.WriteLine("MyServiceB is starting."); + _backgroundTask = BackgroundTask(); + return Task.CompletedTask; + } + + private async Task BackgroundTask() + { + while (!_stopping) + { + await Task.Delay(TimeSpan.FromSeconds(7)); + Console.WriteLine("MyServiceB is doing background work."); + } + + Console.WriteLine("MyServiceB background task is stopping."); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + Console.WriteLine("MyServiceB is stopping."); + _stopping = true; + if (_backgroundTask != null) + { + // TODO: cancellation + await _backgroundTask; + } + } + } +} diff --git a/samples/GenericHostSample/ProgramExternallyControlled.cs b/samples/GenericHostSample/ProgramExternallyControlled.cs new file mode 100644 index 0000000000..749442b161 --- /dev/null +++ b/samples/GenericHostSample/ProgramExternallyControlled.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace GenericHostSample +{ + public class ProgramExternallyControlled + { + private IHost _host; + + public ProgramExternallyControlled() + { + _host = new HostBuilder() + .UseServiceProviderFactory(new MyContainerFactory()) + .ConfigureContainer((hostContext, container) => + { + }) + .ConfigureAppConfiguration((hostContext, config) => + { + config.AddEnvironmentVariables(); + config.AddJsonFile("appsettings.json", optional: true); + }) + .ConfigureServices((hostContext, services) => + { + services.AddScoped(); + services.AddScoped(); + }) + .Build(); + } + + public void Start() + { + _host.Start(); + } + + public async Task StopAsync() + { + await _host.StopAsync(TimeSpan.FromSeconds(5)); + _host.Dispose(); + } + } +} diff --git a/samples/GenericHostSample/ProgramFullControl.cs b/samples/GenericHostSample/ProgramFullControl.cs new file mode 100644 index 0000000000..59400077bb --- /dev/null +++ b/samples/GenericHostSample/ProgramFullControl.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace GenericHostSample +{ + public class ProgramFullControl + { + public static async Task Main(string[] args) + { + var host = new HostBuilder() + .UseServiceProviderFactory(new MyContainerFactory()) + .ConfigureContainer((hostContext, container) => + { + }) + .ConfigureAppConfiguration((hostContext, config) => + { + config.AddEnvironmentVariables(); + config.AddJsonFile("appsettings.json", optional: true); + config.AddCommandLine(args); + }) + .ConfigureServices((hostContext, services) => + { + services.AddScoped(); + services.AddScoped(); + }) + .Build(); + + var s = host.Services; + + using (host) + { + Console.WriteLine("Starting!"); + + await host.StartAsync(); + + Console.WriteLine("Started! Press to stop."); + + Console.ReadLine(); + + Console.WriteLine("Stopping!"); + + await host.StopAsync(); + + Console.WriteLine("Stopped!"); + } + } + } +} diff --git a/samples/GenericHostSample/ProgramHelloWorld.cs b/samples/GenericHostSample/ProgramHelloWorld.cs new file mode 100644 index 0000000000..a145a2534d --- /dev/null +++ b/samples/GenericHostSample/ProgramHelloWorld.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace GenericHostSample +{ + public class ProgramHelloWorld + { + public static async Task Main(string[] args) + { + var builder = new HostBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddScoped(); + services.AddScoped(); + }); + + await builder.RunConsoleAsync(); + } + } +} diff --git a/samples/GenericHostSample/ServiceBaseControlled.cs b/samples/GenericHostSample/ServiceBaseControlled.cs new file mode 100644 index 0000000000..431e061be5 --- /dev/null +++ b/samples/GenericHostSample/ServiceBaseControlled.cs @@ -0,0 +1,23 @@ +#if NET461 +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace GenericHostSample +{ + public class ServiceBaseControlled + { + public static async Task Main(string[] args) + { + var builder = new HostBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddScoped(); + services.AddScoped(); + }); + + await builder.RunAsServiceAsync(); + } + } +} +#endif \ No newline at end of file diff --git a/samples/GenericHostSample/ServiceBaseLifetime.cs b/samples/GenericHostSample/ServiceBaseLifetime.cs new file mode 100644 index 0000000000..08bf4348d0 --- /dev/null +++ b/samples/GenericHostSample/ServiceBaseLifetime.cs @@ -0,0 +1,64 @@ +#if NET461 +using System; +using System.ServiceProcess; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace GenericHostSample +{ + public static class ServiceBaseLifetimeHostExtensions + { + public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton()); + } + + public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default) + { + return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken); + } + } + + public class ServiceBaseLifetime : ServiceBase, IHostLifetime + { + private Action _startCallback; + private Action _stopCallback; + private object _startState; + private object _stopState; + + public void RegisterDelayStartCallback(Action callback, object state) + { + _startCallback = callback ?? throw new ArgumentNullException(nameof(callback)); + _startState = state ?? throw new ArgumentNullException(nameof(state)); + + Run(this); + } + + public void RegisterStopCallback(Action callback, object state) + { + _stopCallback = callback ?? throw new ArgumentNullException(nameof(callback)); + _stopState = state ?? throw new ArgumentNullException(nameof(state)); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Stop(); + return Task.CompletedTask; + } + + protected override void OnStart(string[] args) + { + _startCallback(_startState); + base.OnStart(args); + } + + protected override void OnStop() + { + _stopCallback(_stopState); + base.OnStop(); + } + } +} +#endif \ No newline at end of file diff --git a/samples/GenericWebHost/FakeServer.cs b/samples/GenericWebHost/FakeServer.cs new file mode 100644 index 0000000000..bb1d669846 --- /dev/null +++ b/samples/GenericWebHost/FakeServer.cs @@ -0,0 +1,32 @@ +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 GenericWebHost +{ + // 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 FakeServerWebHostBuilderExtensions + { + public static IHostBuilder UseFakeServer(this IHostBuilder builder) + { + return builder.ConfigureServices((builderContext, services) => services.AddSingleton()); + } + } +} diff --git a/samples/GenericWebHost/GenericWebHost.csproj b/samples/GenericWebHost/GenericWebHost.csproj new file mode 100644 index 0000000000..35eb705695 --- /dev/null +++ b/samples/GenericWebHost/GenericWebHost.csproj @@ -0,0 +1,21 @@ + + + + Exe + netcoreapp2.0;net461 + latest + true + + + + + + + + + + + + + + diff --git a/samples/GenericWebHost/Program.cs b/samples/GenericWebHost/Program.cs new file mode 100644 index 0000000000..4879031f56 --- /dev/null +++ b/samples/GenericWebHost/Program.cs @@ -0,0 +1,41 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace GenericWebHost +{ + public class Program + { + public static async Task Main(string[] args) + { + var host = new HostBuilder() + .ConfigureAppConfiguration((hostContext, config) => + { + config.AddEnvironmentVariables(); + config.AddJsonFile("appsettings.json", optional: true); + config.AddCommandLine(args); + }) + .ConfigureServices((hostContext, services) => + { + }) + .UseFakeServer() + .ConfigureWebHost((hostContext, app) => + { + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + }) + .UseConsoleLifetime() + .Build(); + + var s = host.Services; + + await host.RunAsync(); + } + } +} diff --git a/samples/GenericWebHost/WebHostExtensions.cs b/samples/GenericWebHost/WebHostExtensions.cs new file mode 100644 index 0000000000..2226aba233 --- /dev/null +++ b/samples/GenericWebHost/WebHostExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ObjectPool; + +namespace GenericWebHost +{ + public static class WebHostExtensions + { + public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action configureApp) + { + return builder.ConfigureServices((bulderContext, services) => + { + services.Configure(options => + { + options.ConfigureApp = configureApp; + }); + services.AddSingleton(); + + var listener = new DiagnosticListener("Microsoft.AspNetCore"); + services.AddSingleton(listener); + services.AddSingleton(listener); + + services.AddTransient(); + services.AddScoped(); + + // Conjure up a RequestServices + services.AddTransient(); + services.AddTransient, DefaultServiceProviderFactory>(); + + // Ensure object pooling is available everywhere. + services.AddSingleton(); + }); + } + } +} diff --git a/samples/GenericWebHost/WebHostService.cs b/samples/GenericWebHost/WebHostService.cs new file mode 100644 index 0000000000..1ac316178f --- /dev/null +++ b/samples/GenericWebHost/WebHostService.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace GenericWebHost +{ + internal class WebHostService : IHostedService + { + public WebHostService(IOptions options, IServiceProvider services, HostBuilderContext hostBuilderContext, IServer server, + ILogger logger, DiagnosticListener diagnosticListener, IHttpContextFactory httpContextFactory) + { + Options = options?.Value ?? throw new System.ArgumentNullException(nameof(options)); + + if (Options.ConfigureApp == null) + { + throw new ArgumentException(nameof(Options.ConfigureApp)); + } + + Services = services ?? throw new ArgumentNullException(nameof(services)); + HostBuilderContext = hostBuilderContext ?? throw new ArgumentNullException(nameof(hostBuilderContext)); + Server = server ?? throw new ArgumentNullException(nameof(server)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + DiagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener)); + HttpContextFactory = httpContextFactory ?? throw new ArgumentNullException(nameof(httpContextFactory)); + } + + public WebHostServiceOptions Options { get; } + public IServiceProvider Services { get; } + public HostBuilderContext HostBuilderContext { get; } + public IServer Server { get; } + public ILogger Logger { get; } + public DiagnosticListener DiagnosticListener { get; } + public IHttpContextFactory HttpContextFactory { get; } + + public Task StartAsync(CancellationToken cancellationToken) + { + Server.Features.Get()?.Addresses.Add("http://localhost:5000"); + + var builder = new ApplicationBuilder(Services, Server.Features); + Options.ConfigureApp(HostBuilderContext, builder); + var app = builder.Build(); + + var httpApp = new HostingApplication(app, Logger, DiagnosticListener, HttpContextFactory); + return Server.StartAsync(httpApp, cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Server.StopAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/samples/GenericWebHost/WebHostServiceOptions.cs b/samples/GenericWebHost/WebHostServiceOptions.cs new file mode 100644 index 0000000000..123dcf8790 --- /dev/null +++ b/samples/GenericWebHost/WebHostServiceOptions.cs @@ -0,0 +1,11 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; + +namespace GenericWebHost +{ + public class WebHostServiceOptions + { + public Action ConfigureApp { get; internal set; } + } +} \ No newline at end of file diff --git a/samples/SampleStartups/StartupExternallyControlled.cs b/samples/SampleStartups/StartupExternallyControlled.cs index 4fa9e55b87..68ec11c9b0 100644 --- a/samples/SampleStartups/StartupExternallyControlled.cs +++ b/samples/SampleStartups/StartupExternallyControlled.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/IApplicationLifetime.cs b/src/Microsoft.AspNetCore.Hosting.Abstractions/IApplicationLifetime.cs index 953b29fa6f..f4613dd7d9 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/IApplicationLifetime.cs +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/IApplicationLifetime.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Hosting CancellationToken ApplicationStopped { get; } /// - /// Requests termination the current application. + /// Requests termination of the current application. /// void StopApplication(); } diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/IHostingEnvironment.cs b/src/Microsoft.AspNetCore.Hosting.Abstractions/IHostingEnvironment.cs index a619c7791f..5feeb38eb7 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/IHostingEnvironment.cs +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/IHostingEnvironment.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Hosting public interface IHostingEnvironment { /// - /// Gets or sets the name of the environment. This property is automatically set by the host to the value + /// Gets or sets the name of the environment. The host automatically sets this property to the value /// of the "ASPNETCORE_ENVIRONMENT" environment variable, or "environment" as specified in any other configuration source. /// string EnvironmentName { get; set; } diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/Microsoft.AspNetCore.Hosting.Abstractions.csproj b/src/Microsoft.AspNetCore.Hosting.Abstractions/Microsoft.AspNetCore.Hosting.Abstractions.csproj index 29e9f6a943..557415600c 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/Microsoft.AspNetCore.Hosting.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/Microsoft.AspNetCore.Hosting.Abstractions.csproj @@ -15,10 +15,6 @@ - - - - diff --git a/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostBuilderContext.cs b/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostBuilderContext.cs index 071479c7a8..58e8d0798b 100644 --- a/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostBuilderContext.cs +++ b/src/Microsoft.AspNetCore.Hosting.Abstractions/WebHostBuilderContext.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Hosting { diff --git a/src/Microsoft.AspNetCore.Hosting/Microsoft.AspNetCore.Hosting.csproj b/src/Microsoft.AspNetCore.Hosting/Microsoft.AspNetCore.Hosting.csproj index 38b1d96597..0a8c25a9e5 100644 --- a/src/Microsoft.AspNetCore.Hosting/Microsoft.AspNetCore.Hosting.csproj +++ b/src/Microsoft.AspNetCore.Hosting/Microsoft.AspNetCore.Hosting.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Microsoft.AspNetCore.Hosting/WebHostBuilderExtensions.cs b/src/Microsoft.AspNetCore.Hosting/WebHostBuilderExtensions.cs index fd094379a7..09c7e6d96b 100644 --- a/src/Microsoft.AspNetCore.Hosting/WebHostBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Hosting/WebHostBuilderExtensions.cs @@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Hosting } /// - /// Adds a delegate for configuring the provided . This may be called multiple times. + /// Adds a delegate for configuring the provided . This may be called multiple times. /// /// The to configure. /// The delegate that configures the . diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/EnvironmentName.cs b/src/Microsoft.Extensions.Hosting.Abstractions/EnvironmentName.cs new file mode 100644 index 0000000000..c3b18c5044 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/EnvironmentName.cs @@ -0,0 +1,15 @@ +// 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. + +namespace Microsoft.Extensions.Hosting +{ + /// + /// Commonly used environment names. + /// + public static class EnvironmentName + { + public static readonly string Development = "Development"; + public static readonly string Staging = "Staging"; + public static readonly string Production = "Production"; + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/HostBuilderContext.cs b/src/Microsoft.Extensions.Hosting.Abstractions/HostBuilderContext.cs new file mode 100644 index 0000000000..b30d133ff4 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/HostBuilderContext.cs @@ -0,0 +1,34 @@ +// 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.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Hosting +{ + /// + /// Context containing the common services on the . Some properties may be null until set by the . + /// + public class HostBuilderContext + { + public HostBuilderContext(IDictionary properties) + { + Properties = properties ?? throw new System.ArgumentNullException(nameof(properties)); + } + + /// + /// The initialized by the . + /// + public IHostingEnvironment HostingEnvironment { get; set; } + + /// + /// The containing the merged configuration of the application and the . + /// + public IConfiguration Configuration { get; set; } + + /// + /// A central location for sharing state between components during the host building process. + /// + public IDictionary Properties { get; } + } +} diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/HostDefaults.cs b/src/Microsoft.Extensions.Hosting.Abstractions/HostDefaults.cs new file mode 100644 index 0000000000..e39be7330f --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/HostDefaults.cs @@ -0,0 +1,27 @@ +// 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. + +namespace Microsoft.Extensions.Hosting +{ + /// + /// Constants for HostBuilder configuration keys. + /// + public static class HostDefaults + { + /// + /// The configuration key used to set . + /// + public static readonly string ApplicationKey = "applicationName"; + + /// + /// The configuration key used to set . + /// + public static readonly string EnvironmentKey = "environment"; + + /// + /// The configuration key used to set + /// and . + /// + public static readonly string ContentRootKey = "contentRoot"; + } +} diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/HostingAbstractionsHostBuilderExtensions.cs b/src/Microsoft.Extensions.Hosting.Abstractions/HostingAbstractionsHostBuilderExtensions.cs new file mode 100644 index 0000000000..451f4cf32c --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/HostingAbstractionsHostBuilderExtensions.cs @@ -0,0 +1,22 @@ +// 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.Extensions.Hosting +{ + public static class HostingAbstractionsHostBuilderExtensions + { + /// + /// Start the web host and listen on the specified urls. + /// + /// The to start. + /// The . + public static IHost Start(this IHostBuilder hostBuilder) + { + var host = hostBuilder.Build(); + host.StartAsync(CancellationToken.None).GetAwaiter().GetResult(); + return host; + } + } +} diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/HostingAbstractionsHostExtensions.cs b/src/Microsoft.Extensions.Hosting.Abstractions/HostingAbstractionsHostExtensions.cs new file mode 100644 index 0000000000..8bc47d7b02 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/HostingAbstractionsHostExtensions.cs @@ -0,0 +1,95 @@ +// 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.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Hosting +{ + public static class HostingAbstractionsHostExtensions + { + /// + /// Starts the host synchronously. + /// + /// + public static void Start(this IHost host) + { + host.StartAsync().GetAwaiter().GetResult(); + } + + /// + /// Attempts to gracefully stop the host with the given timeout. + /// + /// + /// The timeout for stopping gracefully. Once expired the + /// server may terminate any remaining active connections. + /// + public static Task StopAsync(this IHost host, TimeSpan timeout) + { + return host.StopAsync(new CancellationTokenSource(timeout).Token); + } + + /// + /// Block the calling thread until shutdown is triggered via Ctrl+C or SIGTERM. + /// + /// The running . + public static void WaitForShutdown(this IHost host) + { + host.WaitForShutdownAsync().GetAwaiter().GetResult(); + } + + /// + /// Runs a web application and block the calling thread until host shutdown. + /// + /// The to run. + public static void Run(this IHost host) + { + host.RunAsync().GetAwaiter().GetResult(); + } + + /// + /// Runs a web application and returns a Task that only completes when the token is triggered or shutdown is triggered. + /// + /// The to run. + /// The token to trigger shutdown. + public static async Task RunAsync(this IHost host, CancellationToken token = default(CancellationToken)) + { + using (host) + { + await host.StartAsync(token); + + await host.WaitForShutdownAsync(token); + } + } + + /// + /// Returns a Task that completes when shutdown is triggered via the given token. + /// + /// The running . + /// The token to trigger shutdown. + public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default(CancellationToken)) + { + var applicationLifetime = host.Services.GetService(); + + token.Register(state => + { + ((IApplicationLifetime)state).StopApplication(); + }, + applicationLifetime); + + var waitForStop = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + applicationLifetime.ApplicationStopping.Register(obj => + { + var tcs = (TaskCompletionSource)obj; + tcs.TrySetResult(null); + }, waitForStop); + + await waitForStop.Task; + + // WebHost will use its default ShutdownTimeout if none is specified. + await host.StopAsync(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/HostingEnvironmentExtensions.cs b/src/Microsoft.Extensions.Hosting.Abstractions/HostingEnvironmentExtensions.cs new file mode 100644 index 0000000000..1b1e49aa2f --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/HostingEnvironmentExtensions.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; + +namespace Microsoft.Extensions.Hosting +{ + /// + /// Extension methods for . + /// + public static class HostingEnvironmentExtensions + { + /// + /// Checks if the current hosting environment name is . + /// + /// An instance of . + /// True if the environment name is , otherwise false. + public static bool IsDevelopment(this IHostingEnvironment hostingEnvironment) + { + if (hostingEnvironment == null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + return hostingEnvironment.IsEnvironment(EnvironmentName.Development); + } + + /// + /// Checks if the current hosting environment name is . + /// + /// An instance of . + /// True if the environment name is , otherwise false. + public static bool IsStaging(this IHostingEnvironment hostingEnvironment) + { + if (hostingEnvironment == null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + return hostingEnvironment.IsEnvironment(EnvironmentName.Staging); + } + + /// + /// Checks if the current hosting environment name is . + /// + /// An instance of . + /// True if the environment name is , otherwise false. + public static bool IsProduction(this IHostingEnvironment hostingEnvironment) + { + if (hostingEnvironment == null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + return hostingEnvironment.IsEnvironment(EnvironmentName.Production); + } + + /// + /// Compares the current hosting environment name against the specified value. + /// + /// An instance of . + /// Environment name to validate against. + /// True if the specified name is the same as the current environment, otherwise false. + public static bool IsEnvironment( + this IHostingEnvironment hostingEnvironment, + string environmentName) + { + if (hostingEnvironment == null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + return string.Equals( + hostingEnvironment.EnvironmentName, + environmentName, + StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/IApplicationLifetime.cs b/src/Microsoft.Extensions.Hosting.Abstractions/IApplicationLifetime.cs new file mode 100644 index 0000000000..07f8249a26 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/IApplicationLifetime.cs @@ -0,0 +1,37 @@ +// 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.Extensions.Hosting +{ + /// + /// Allows consumers to perform cleanup during a graceful shutdown. + /// + public interface IApplicationLifetime + { + /// + /// Triggered when the application host has fully started and is about to wait + /// for a graceful shutdown. + /// + CancellationToken ApplicationStarted { get; } + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// Requests may still be in flight. Shutdown will block until this event completes. + /// + CancellationToken ApplicationStopping { get; } + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// All requests should be complete at this point. Shutdown will block + /// until this event completes. + /// + CancellationToken ApplicationStopped { get; } + + /// + /// Requests termination of the current application. + /// + void StopApplication(); + } +} diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/IHost.cs b/src/Microsoft.Extensions.Hosting.Abstractions/IHost.cs new file mode 100644 index 0000000000..bd1e9f14bf --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/IHost.cs @@ -0,0 +1,34 @@ +// 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; + +namespace Microsoft.Extensions.Hosting +{ + /// + /// A program abstraction. + /// + public interface IHost : IDisposable + { + /// + /// The programs configured services. + /// + IServiceProvider Services { get; } + + /// + /// Start the program. + /// + /// Used to abort program start. + /// + Task StartAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Attempts to gracefully stop the program. + /// + /// Used to indicate when stop should no longer be graceful. + /// + Task StopAsync(CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/IHostBuilder.cs b/src/Microsoft.Extensions.Hosting.Abstractions/IHostBuilder.cs new file mode 100644 index 0000000000..66fbbea09f --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/IHostBuilder.cs @@ -0,0 +1,71 @@ +// 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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Hosting +{ + /// + /// A program initialization abstraction. + /// + public interface IHostBuilder + { + /// + /// A central location for sharing state between components during the host building process. + /// + IDictionary Properties { get; } + + /// + /// Set up the configuration for the builder itself. This will be used to initialize the + /// for use later in the build process. This can be called multiple times and the results will be additive. + /// + /// The delegate for configuring the that will be used + /// to construct the for the host. + /// The same instance of the for chaining. + IHostBuilder ConfigureHostConfiguration(Action configureDelegate); + + /// + /// Sets up the configuration for the remainder of the build process and application. This can be called multiple times and + /// the results will be additive. The results will be available at for + /// subsequent operations, as well as in . + /// + /// The delegate for configuring the that will be used + /// to construct the for the application. + /// The same instance of the for chaining. + IHostBuilder ConfigureAppConfiguration(Action configureDelegate); + + /// + /// Adds services to the container. This can be called multiple times and the results will be additive. + /// + /// The delegate for configuring the that will be used + /// to construct the . + /// The same instance of the for chaining. + IHostBuilder ConfigureServices(Action configureDelegate); + + /// + /// Overrides the factory used to create the service provider. + /// + /// + /// + /// The same instance of the for chaining. + IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory); + + /// + /// Enables configuring the instantiated dependency container. This can be called multiple times and + /// the results will be additive. + /// + /// + /// + /// The same instance of the for chaining. + IHostBuilder ConfigureContainer(Action configureDelegate); + + /// + /// Run the given actions to initialize the host. This can only be called once. + /// + /// An initialized + IHost Build(); + } +} diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/IHostLifetime.cs b/src/Microsoft.Extensions.Hosting.Abstractions/IHostLifetime.cs new file mode 100644 index 0000000000..ee89a3bd23 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/IHostLifetime.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; + +namespace Microsoft.Extensions.Hosting +{ + public interface IHostLifetime + { + /// + /// Called at the start of which will wait until the callback is invoked before + /// continuing. This can be used to delay startup until signaled by an external event. + /// + /// A callback that will be invoked when the host should continue. + /// State to pass to the callback. + void RegisterDelayStartCallback(Action callback, object state); + + /// + /// Called at the start of to register the given callback for initiating the + /// application shutdown process. + /// + /// A callback to invoke when an external signal indicates the application should stop. + /// State to pass to the callback. + void RegisterStopCallback(Action callback, object state); + + /// + /// Called from to indicate that the host as stopped and clean up resources. + /// + /// Used to indicate when stop should no longer be graceful. + /// + Task StopAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/IHostedService.cs b/src/Microsoft.Extensions.Hosting.Abstractions/IHostedService.cs index 4fb372ca2b..bc83fd0f99 100644 --- a/src/Microsoft.Extensions.Hosting.Abstractions/IHostedService.cs +++ b/src/Microsoft.Extensions.Hosting.Abstractions/IHostedService.cs @@ -14,11 +14,13 @@ namespace Microsoft.Extensions.Hosting /// /// Triggered when the application host is ready to start the service. /// + /// Indicates that the start process has been aborted. Task StartAsync(CancellationToken cancellationToken); /// /// Triggered when the application host is performing a graceful shutdown. /// + /// Indicates that the shutdown process should no longer be graceful. Task StopAsync(CancellationToken cancellationToken); } } diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/IHostingEnvironment.cs b/src/Microsoft.Extensions.Hosting.Abstractions/IHostingEnvironment.cs new file mode 100644 index 0000000000..a2614b12f4 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting.Abstractions/IHostingEnvironment.cs @@ -0,0 +1,36 @@ +// 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.Collections.Generic; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.Extensions.Hosting +{ + /// + /// Provides information about the web hosting environment an application is running in. + /// + public interface IHostingEnvironment + { + /// + /// Gets or sets the name of the environment. The host automatically sets this property to the value of the + /// of the "environment" key as specified in configuration. + /// + string EnvironmentName { get; set; } + + /// + /// Gets or sets the name of the application. This property is automatically set by the host to the assembly containing + /// the application entry point. + /// + string ApplicationName { get; set; } + + /// + /// Gets or sets the absolute path to the directory that contains the application content files. + /// + string ContentRootPath { get; set; } + + /// + /// Gets or sets an pointing at . + /// + IFileProvider ContentRootFileProvider { get; set; } + } +} diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/Microsoft.Extensions.Hosting.Abstractions.csproj b/src/Microsoft.Extensions.Hosting.Abstractions/Microsoft.Extensions.Hosting.Abstractions.csproj index cf89a1f216..8780a85be4 100644 --- a/src/Microsoft.Extensions.Hosting.Abstractions/Microsoft.Extensions.Hosting.Abstractions.csproj +++ b/src/Microsoft.Extensions.Hosting.Abstractions/Microsoft.Extensions.Hosting.Abstractions.csproj @@ -7,6 +7,14 @@ true hosting false + Microsoft.Extensions.Hosting + + + + + + + diff --git a/src/Microsoft.Extensions.Hosting/ConsoleLifetimeOptions.cs b/src/Microsoft.Extensions.Hosting/ConsoleLifetimeOptions.cs new file mode 100644 index 0000000000..fedc0e6fb8 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/ConsoleLifetimeOptions.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. + +namespace Microsoft.Extensions.Hosting +{ + public class ConsoleLifetimeOptions + { + /// + /// Indicates if host lifetime status messages should be written to the console such as on startup. + /// The default is true. + /// + public bool WriteStatusMessages { get; set; } = true; + } +} diff --git a/src/Microsoft.Extensions.Hosting/HostBuilder.cs b/src/Microsoft.Extensions.Hosting/HostBuilder.cs new file mode 100644 index 0000000000..c8eac6aa52 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/HostBuilder.cs @@ -0,0 +1,205 @@ +// 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.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting.Internal; + +namespace Microsoft.Extensions.Hosting +{ + /// + /// A program initialization utility. + /// + public class HostBuilder : IHostBuilder + { + private List> _configureHostConfigActions = new List>(); + private List> _configureAppConfigActions = new List>(); + private List> _configureServicesActions = new List>(); + private List _configureContainerActions = new List(); + private IServiceFactoryAdapter _serviceProviderFactory = new ServiceFactoryAdapter(new DefaultServiceProviderFactory()); + private bool _hostBuilt; + private IConfiguration _hostConfiguration; + private IConfiguration _appConfiguration; + private HostBuilderContext _hostBuilderContext; + private IHostingEnvironment _hostingEnvironment; + private IServiceProvider _appServices; + + /// + /// A central location for sharing state between components during the host building process. + /// + public IDictionary Properties => new Dictionary(); + + /// + /// Set up the configuration for the builder itself. This will be used to initialize the + /// for use later in the build process. This can be called multiple times and the results will be additive. + /// + /// + /// The same instance of the for chaining. + public IHostBuilder ConfigureHostConfiguration(Action configureDelegate) + { + _configureHostConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); + return this; + } + + /// + /// Sets up the configuration for the remainder of the build process and application. This can be called multiple times and + /// the results will be additive. The results will be available at for + /// subsequent operations, as well as in . + /// + /// + /// The same instance of the for chaining. + public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); + return this; + } + + /// + /// Adds services to the container. This can be called multiple times and the results will be additive. + /// + /// + /// The same instance of the for chaining. + public IHostBuilder ConfigureServices(Action configureDelegate) + { + _configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); + return this; + } + + /// + /// Overrides the factory used to create the service provider. + /// + /// + /// + /// The same instance of the for chaining. + public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) + { + _serviceProviderFactory = new ServiceFactoryAdapter(factory ?? throw new ArgumentNullException(nameof(factory))); + return this; + } + + /// + /// Enables configuring the instantiated dependency container. This can be called multiple times and + /// the results will be additive. + /// + /// + /// + /// The same instance of the for chaining. + public IHostBuilder ConfigureContainer(Action configureDelegate) + { + _configureContainerActions.Add(new ConfigureContainerAdapter(configureDelegate + ?? throw new ArgumentNullException(nameof(configureDelegate)))); + return this; + } + + /// + /// Run the given actions to initialize the host. This can only be called once. + /// + /// An initialized + public IHost Build() + { + if (_hostBuilt) + { + throw new InvalidOperationException("Build can only be called once."); + } + _hostBuilt = true; + + BuildHostConfiguration(); + CreateHostingEnvironment(); + CreateHostBuilderContext(); + BuildAppConfiguration(); + CreateServiceProvider(); + + return new Host(_appServices); + } + + private void BuildHostConfiguration() + { + var configBuilder = new ConfigurationBuilder(); + foreach (var buildAction in _configureHostConfigActions) + { + buildAction(configBuilder); + } + _hostConfiguration = configBuilder.Build(); + } + + private void CreateHostingEnvironment() + { + _hostingEnvironment = new HostingEnvironment() + { + ApplicationName = _hostConfiguration[HostDefaults.ApplicationKey], + EnvironmentName = _hostConfiguration[HostDefaults.EnvironmentKey] ?? EnvironmentName.Production, + ContentRootPath = ResolveContentRootPath(_hostConfiguration[HostDefaults.ContentRootKey], AppContext.BaseDirectory), + }; + _hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(_hostingEnvironment.ContentRootPath); + } + + private string ResolveContentRootPath(string contentRootPath, string basePath) + { + if (string.IsNullOrEmpty(contentRootPath)) + { + return basePath; + } + if (Path.IsPathRooted(contentRootPath)) + { + return contentRootPath; + } + return Path.Combine(Path.GetFullPath(basePath), contentRootPath); + } + + private void CreateHostBuilderContext() + { + _hostBuilderContext = new HostBuilderContext(Properties) + { + HostingEnvironment = _hostingEnvironment, + Configuration = _hostConfiguration + }; + } + + private void BuildAppConfiguration() + { + var configBuilder = new ConfigurationBuilder(); + configBuilder.AddConfiguration(_hostConfiguration); + foreach (var buildAction in _configureAppConfigActions) + { + buildAction(_hostBuilderContext, configBuilder); + } + _appConfiguration = configBuilder.Build(); + _hostBuilderContext.Configuration = _appConfiguration; + } + + private void CreateServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(_hostingEnvironment); + services.AddSingleton(_hostBuilderContext); + services.AddSingleton(_appConfiguration); + services.AddSingleton(); + services.AddSingleton(); + services.AddOptions(); + services.AddLogging(); + + foreach (var configureServicesAction in _configureServicesActions) + { + configureServicesAction(_hostBuilderContext, services); + } + + var containerBuilder = _serviceProviderFactory.CreateBuilder(services); + + foreach (var containerAction in _configureContainerActions) + { + containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder); + } + + _appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder); + + if (_appServices == null) + { + throw new InvalidOperationException($"The IServiceProviderFactory returned a null IServiceProvider."); + } + } + } +} diff --git a/src/Microsoft.Extensions.Hosting/HostingHostBuilderExtensions.cs b/src/Microsoft.Extensions.Hosting/HostingHostBuilderExtensions.cs new file mode 100644 index 0000000000..55125cb888 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/HostingHostBuilderExtensions.cs @@ -0,0 +1,86 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Internal; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Hosting +{ + public static class HostingHostBuilderExtensions + { + /// + /// Specify the environment to be used by the web host. + /// + /// The to configure. + /// The environment to host the application in. + /// The . + public static IHostBuilder UseEnvironment(this IHostBuilder hostBuilder, string environment) + { + return hostBuilder.ConfigureHostConfiguration(configBuilder => + { + configBuilder.AddInMemoryCollection(new[] + { + new KeyValuePair(HostDefaults.EnvironmentKey, + environment ?? throw new ArgumentNullException(nameof(environment))) + }); + }); + } + + /// + /// Specify the content root directory to be used by the web host. + /// + /// The to configure. + /// Path to root directory of the application. + /// The . + public static IHostBuilder UseContentRoot(this IHostBuilder hostBuilder, string contentRoot) + { + return hostBuilder.ConfigureHostConfiguration(configBuilder => + { + configBuilder.AddInMemoryCollection(new[] + { + new KeyValuePair(HostDefaults.ContentRootKey, + contentRoot ?? throw new ArgumentNullException(nameof(contentRoot))) + }); + }); + } + + /// + /// Adds a delegate for configuring the provided . This may be called multiple times. + /// + /// The to configure. + /// The delegate that configures the . + /// The same instance of the for chaining. + public static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder, Action configureLogging) + { + return hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder))); + } + + /// + /// Listens for Ctrl+C or SIGTERM and calls to start the shutdown process. + /// This will unblock extensions like RunAsync and WaitForShutdownAsync. + /// + /// The to configure. + /// The same instance of the for chaining. + public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton()); + } + + /// + /// Enables console support, builds and starts the host, and waits for Ctrl+C or SIGTERM to shut down. + /// + /// The to configure. + /// + /// + public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default(CancellationToken)) + { + return hostBuilder.UseConsoleLifetime().Build().RunAsync(cancellationToken); + } + } +} diff --git a/src/Microsoft.Extensions.Hosting/Internal/ApplicationLifetime.cs b/src/Microsoft.Extensions.Hosting/Internal/ApplicationLifetime.cs new file mode 100644 index 0000000000..d94064a1e7 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/ApplicationLifetime.cs @@ -0,0 +1,134 @@ +// 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.Threading; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Hosting.Internal +{ + /// + /// Allows consumers to perform cleanup during a graceful shutdown. + /// + public class ApplicationLifetime : IApplicationLifetime + { + private readonly CancellationTokenSource _startedSource = new CancellationTokenSource(); + private readonly CancellationTokenSource _stoppingSource = new CancellationTokenSource(); + private readonly CancellationTokenSource _stoppedSource = new CancellationTokenSource(); + private readonly ILogger _logger; + + public ApplicationLifetime(ILogger logger) + { + _logger = logger; + } + + /// + /// Triggered when the application host has fully started and is about to wait + /// for a graceful shutdown. + /// + public CancellationToken ApplicationStarted => _startedSource.Token; + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// Request may still be in flight. Shutdown will block until this event completes. + /// + public CancellationToken ApplicationStopping => _stoppingSource.Token; + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// All requests should be complete at this point. Shutdown will block + /// until this event completes. + /// + public CancellationToken ApplicationStopped => _stoppedSource.Token; + + /// + /// Signals the ApplicationStopping event and blocks until it completes. + /// + public void StopApplication() + { + // Lock on CTS to synchronize multiple calls to StopApplication. This guarantees that the first call + // to StopApplication and its callbacks run to completion before subsequent calls to StopApplication, + // which will no-op since the first call already requested cancellation, get a chance to execute. + lock (_stoppingSource) + { + try + { + ExecuteHandlers(_stoppingSource); + } + catch (Exception ex) + { + _logger.ApplicationError(LoggerEventIds.ApplicationStoppingException, + "An error occurred stopping the application", + ex); + } + } + } + + /// + /// Signals the ApplicationStarted event and blocks until it completes. + /// + public void NotifyStarted() + { + try + { + ExecuteHandlers(_startedSource); + } + catch (Exception ex) + { + _logger.ApplicationError(LoggerEventIds.ApplicationStartupException, + "An error occurred starting the application", + ex); + } + } + + /// + /// Signals the ApplicationStopped event and blocks until it completes. + /// + public void NotifyStopped() + { + try + { + ExecuteHandlers(_stoppedSource); + } + catch (Exception ex) + { + _logger.ApplicationError(LoggerEventIds.ApplicationStoppedException, + "An error occurred stopping the application", + ex); + } + } + + private void ExecuteHandlers(CancellationTokenSource cancel) + { + // Noop if this is already cancelled + if (cancel.IsCancellationRequested) + { + return; + } + + List exceptions = null; + + try + { + // Run the cancellation token callbacks + cancel.Cancel(throwOnFirstException: false); + } + catch (Exception ex) + { + if (exceptions == null) + { + exceptions = new List(); + } + + exceptions.Add(ex); + } + + // Throw an aggregate exception if there were any exceptions + if (exceptions != null) + { + throw new AggregateException(exceptions); + } + } + } +} diff --git a/src/Microsoft.Extensions.Hosting/Internal/ConfigureContainerAdapter.cs b/src/Microsoft.Extensions.Hosting/Internal/ConfigureContainerAdapter.cs new file mode 100644 index 0000000000..6b443b0e73 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/ConfigureContainerAdapter.cs @@ -0,0 +1,22 @@ +// 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; + +namespace Microsoft.Extensions.Hosting.Internal +{ + internal class ConfigureContainerAdapter : IConfigureContainerAdapter + { + private Action _action; + + public ConfigureContainerAdapter(Action action) + { + _action = action ?? throw new ArgumentNullException(nameof(action)); + } + + public void ConfigureContainer(HostBuilderContext hostContext, object containerBuilder) + { + _action(hostContext, (TContainerBuilder)containerBuilder); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting/Internal/ConsoleLifetime.cs b/src/Microsoft.Extensions.Hosting/Internal/ConsoleLifetime.cs new file mode 100644 index 0000000000..d801786c2d --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/ConsoleLifetime.cs @@ -0,0 +1,61 @@ +// 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.Extensions.Options; + +namespace Microsoft.Extensions.Hosting.Internal +{ + /// + /// Listens for Ctrl+C or SIGTERM and initiates shutdown. + /// + public class ConsoleLifetime : IHostLifetime + { + public ConsoleLifetime(IOptions options, IHostingEnvironment environment, IApplicationLifetime applicationLifetime) + { + Options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + Environment = environment ?? throw new ArgumentNullException(nameof(environment)); + ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); + } + + private ConsoleLifetimeOptions Options { get; } + + private IHostingEnvironment Environment { get; } + + private IApplicationLifetime ApplicationLifetime { get; } + + public void RegisterDelayStartCallback(Action callback, object state) + { + if (Options.WriteStatusMessages) + { + ApplicationLifetime.ApplicationStarted.Register(() => + { + Console.WriteLine("Application started. Press Ctrl+C to shut down."); + Console.WriteLine($"Hosting environment: {Environment.EnvironmentName}"); + Console.WriteLine($"Content root path: {Environment.ContentRootPath}"); + }); + } + + // Console applications start immediately. + callback(state); + } + + public void RegisterStopCallback(Action callback, object state) + { + AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => callback(state); + Console.CancelKeyPress += (sender, e) => + { + e.Cancel = true; + callback(state); + }; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + // There's nothing to do here + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.Extensions.Hosting/Internal/Host.cs b/src/Microsoft.Extensions.Hosting/Internal/Host.cs new file mode 100644 index 0000000000..b009767159 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/Host.cs @@ -0,0 +1,99 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Hosting.Internal +{ + internal class Host : IHost + { + private ILogger _logger; + private IHostLifetime _hostLifetime; + private ApplicationLifetime _applicationLifetime; + private IEnumerable _hostedServices; + + internal Host(IServiceProvider services) + { + Services = services ?? throw new ArgumentNullException(nameof(services)); + _applicationLifetime = Services.GetRequiredService() as ApplicationLifetime; + _logger = Services.GetRequiredService>(); + _hostLifetime = Services.GetRequiredService(); + } + + public IServiceProvider Services { get; } + + public async Task StartAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + _logger.Starting(); + + var delayStart = new TaskCompletionSource(); + cancellationToken.Register(obj => ((TaskCompletionSource)obj).TrySetCanceled(), delayStart); + _hostLifetime.RegisterDelayStartCallback(obj => ((TaskCompletionSource)obj).TrySetResult(null), delayStart); + _hostLifetime.RegisterStopCallback(obj => (obj as IApplicationLifetime)?.StopApplication(), _applicationLifetime); + + await delayStart.Task; + + _hostedServices = Services.GetService>(); + + foreach (var hostedService in _hostedServices) + { + // Fire IHostedService.Start + await hostedService.StartAsync(cancellationToken).ConfigureAwait(false); + } + + // Fire IApplicationLifetime.Started + _applicationLifetime?.NotifyStarted(); + + _logger.Started(); + } + + public async Task StopAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + _logger.Stopping(); + + // Trigger IApplicationLifetime.ApplicationStopping + _applicationLifetime?.StopApplication(); + + IList exceptions = new List(); + if (_hostedServices != null) // Started? + { + foreach (var hostedService in _hostedServices.Reverse()) + { + try + { + await hostedService.StopAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + } + + await _hostLifetime.StopAsync(cancellationToken); + + // Fire IApplicationLifetime.Stopped + _applicationLifetime?.NotifyStopped(); + + if (exceptions.Count > 0) + { + var ex = new AggregateException("One or more hosted services failed to stop.", exceptions); + _logger.StoppedWithException(ex); + throw ex; + } + + _logger.Stopped(); + } + + public void Dispose() + { + (Services as IDisposable)?.Dispose(); + } + } +} diff --git a/src/Microsoft.Extensions.Hosting/Internal/HostProcessLifetime.cs b/src/Microsoft.Extensions.Hosting/Internal/HostProcessLifetime.cs new file mode 100644 index 0000000000..09e2870d7f --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/HostProcessLifetime.cs @@ -0,0 +1,28 @@ +// 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; + +namespace Microsoft.Extensions.Hosting.Internal +{ + public class HostProcessLifetime : IHostLifetime + { + public void RegisterDelayStartCallback(Action callback, object state) + { + // Never delays start. + callback(state); + } + + public void RegisterStopCallback(Action callback, object state) + { + AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => callback(state); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting/Internal/HostingEnvironment.cs b/src/Microsoft.Extensions.Hosting/Internal/HostingEnvironment.cs new file mode 100644 index 0000000000..8227718eee --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/HostingEnvironment.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.Extensions.FileProviders; + +namespace Microsoft.Extensions.Hosting.Internal +{ + public class HostingEnvironment : IHostingEnvironment + { + public string EnvironmentName { get; set; } + + public string ApplicationName { get; set; } + + public string ContentRootPath { get; set; } + + public IFileProvider ContentRootFileProvider { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting/Internal/HostingLoggerExtensions.cs b/src/Microsoft.Extensions.Hosting/Internal/HostingLoggerExtensions.cs new file mode 100644 index 0000000000..ffc5c8288a --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/HostingLoggerExtensions.cs @@ -0,0 +1,84 @@ +// 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; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Hosting.Internal +{ + internal static class HostingLoggerExtensions + { + public static void ApplicationError(this ILogger logger, EventId eventId, string message, Exception exception) + { + var reflectionTypeLoadException = exception as ReflectionTypeLoadException; + if (reflectionTypeLoadException != null) + { + foreach (var ex in reflectionTypeLoadException.LoaderExceptions) + { + message = message + Environment.NewLine + ex.Message; + } + } + + logger.LogCritical( + eventId: eventId, + message: message, + exception: exception); + } + + public static void Starting(this ILogger logger) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.Starting, + message: "Hosting starting"); + } + } + + public static void Started(this ILogger logger) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.Started, + message: "Hosting started"); + } + } + + public static void Stopping(this ILogger logger) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.Stopping, + message: "Hosting stopping"); + } + } + + public static void Stopped(this ILogger logger) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.Stopped, + message: "Hosting stopped"); + } + } + + public static void StoppedWithException(this ILogger logger, Exception ex) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + eventId: LoggerEventIds.StoppedWithException, + exception: ex, + message: "Hosting shutdown exception"); + } + } + } +} + diff --git a/src/Microsoft.Extensions.Hosting/Internal/IConfigureContainerAdapter.cs b/src/Microsoft.Extensions.Hosting/Internal/IConfigureContainerAdapter.cs new file mode 100644 index 0000000000..b517137243 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/IConfigureContainerAdapter.cs @@ -0,0 +1,10 @@ +// 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. + +namespace Microsoft.Extensions.Hosting.Internal +{ + internal interface IConfigureContainerAdapter + { + void ConfigureContainer(HostBuilderContext hostContext, object containerBuilder); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting/Internal/IServiceFactoryAdapter.cs b/src/Microsoft.Extensions.Hosting/Internal/IServiceFactoryAdapter.cs new file mode 100644 index 0000000000..ba409d6b46 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/IServiceFactoryAdapter.cs @@ -0,0 +1,15 @@ +// 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.Extensions.Hosting.Internal +{ + internal interface IServiceFactoryAdapter + { + object CreateBuilder(IServiceCollection services); + + IServiceProvider CreateServiceProvider(object containerBuilder); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting/Internal/LoggerEventIds.cs b/src/Microsoft.Extensions.Hosting/Internal/LoggerEventIds.cs new file mode 100644 index 0000000000..9ab9bdc6d0 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/LoggerEventIds.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. + +namespace Microsoft.Extensions.Hosting.Internal +{ + internal static class LoggerEventIds + { + public const int Starting = 1; + public const int Started = 2; + public const int Stopping = 3; + public const int Stopped = 4; + public const int StoppedWithException = 5; + public const int ApplicationStartupException = 6; + public const int ApplicationStoppingException = 7; + public const int ApplicationStoppedException = 8; + } +} diff --git a/src/Microsoft.Extensions.Hosting/Internal/ServiceFactoryAdapter.cs b/src/Microsoft.Extensions.Hosting/Internal/ServiceFactoryAdapter.cs new file mode 100644 index 0000000000..39cca44c7e --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Internal/ServiceFactoryAdapter.cs @@ -0,0 +1,28 @@ +// 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.Extensions.Hosting.Internal +{ + internal class ServiceFactoryAdapter : IServiceFactoryAdapter + { + private IServiceProviderFactory _serviceProviderFactory; + + public ServiceFactoryAdapter(IServiceProviderFactory serviceProviderFactory) + { + _serviceProviderFactory = serviceProviderFactory ?? throw new System.ArgumentNullException(nameof(serviceProviderFactory)); + } + + public object CreateBuilder(IServiceCollection services) + { + return _serviceProviderFactory.CreateBuilder(services); + } + + public IServiceProvider CreateServiceProvider(object containerBuilder) + { + return _serviceProviderFactory.CreateServiceProvider((TContainerBuilder)containerBuilder); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting/Microsoft.Extensions.Hosting.csproj b/src/Microsoft.Extensions.Hosting/Microsoft.Extensions.Hosting.csproj new file mode 100644 index 0000000000..743f8aca91 --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/Microsoft.Extensions.Hosting.csproj @@ -0,0 +1,23 @@ + + + + .NET Core hosting and startup infrastructures for applications. + netstandard2.0 + $(NoWarn);CS1591 + true + hosting + false + + + + + + + + + + + + + + diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostLifetime.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostLifetime.cs new file mode 100644 index 0000000000..2e998dc708 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostLifetime.cs @@ -0,0 +1,39 @@ +// 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; + +namespace Microsoft.Extensions.Hosting.Tests.Fakes +{ + public class FakeHostLifetime : IHostLifetime + { + public int StartCount { get; internal set; } + public int StoppingCount { get; internal set; } + public int StopCount { get; internal set; } + + public Action, object> StartAction { get; set; } + public Action, object> StoppingAction { get; set; } + public Action StopAction { get; set; } + + public void RegisterDelayStartCallback(Action callback, object state) + { + StartCount++; + StartAction?.Invoke(callback, state); + } + + public void RegisterStopCallback(Action callback, object state) + { + StoppingCount++; + StoppingAction?.Invoke(callback, state); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + StopCount++; + StopAction?.Invoke(); + return Task.CompletedTask; + } + } +} diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostedService.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostedService.cs new file mode 100644 index 0000000000..d867574269 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostedService.cs @@ -0,0 +1,40 @@ +// 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; + +namespace Microsoft.Extensions.Hosting.Tests.Fakes +{ + public class FakeHostedService : IHostedService, IDisposable + { + public int StartCount { get; internal set; } + public int StopCount { get; internal set; } + public int DisposeCount { get; internal set; } + + public Action StartAction { get; set; } + public Action StopAction { get; set; } + public Action DisposeAction { get; set; } + + public Task StartAsync(CancellationToken cancellationToken) + { + StartCount++; + StartAction?.Invoke(cancellationToken); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + StopCount++; + StopAction?.Invoke(cancellationToken); + return Task.CompletedTask; + } + + public void Dispose() + { + DisposeCount++; + DisposeAction?.Invoke(); + } + } +} diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeOptions.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeOptions.cs new file mode 100644 index 0000000000..730e1ed5b8 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.Hosting.Fakes +{ + public class FakeOptions + { + public bool Configured { get; set; } + public string Environment { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeService.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeService.cs new file mode 100644 index 0000000000..3e24f22d7b --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeService.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; + +namespace Microsoft.Extensions.Hosting.Fakes +{ + public class FakeService : IFakeEveryService, IDisposable + { + public bool Disposed { get; private set; } + + public void Dispose() + { + Disposed = true; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeServiceCollection.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeServiceCollection.cs new file mode 100644 index 0000000000..a0de90ef19 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeServiceCollection.cs @@ -0,0 +1,41 @@ +// 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.Extensions.Hosting.Fakes +{ + public class FakeServiceCollection : IServiceProvider + { + private IServiceProvider _inner; + private IServiceCollection _services; + + public bool FancyMethodCalled { get; private set; } + + public IServiceCollection Services => _services; + + public string State { get; set; } + + public object GetService(Type serviceType) + { + return _inner.GetService(serviceType); + } + + public void Populate(IServiceCollection services) + { + _services = services; + _services.AddSingleton(this); + } + + public void Build() + { + _inner = _services.BuildServiceProvider(); + } + + public void MyFancyContainerMethod() + { + FancyMethodCalled = true; + } + } +} diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeServiceProviderFactory.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeServiceProviderFactory.cs new file mode 100644 index 0000000000..5e7b1db3b1 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeServiceProviderFactory.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; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Hosting.Fakes +{ + public class FakeServiceProviderFactory : IServiceProviderFactory + { + public FakeServiceCollection CreateBuilder(IServiceCollection services) + { + var container = new FakeServiceCollection(); + container.Populate(services); + return container; + } + + public IServiceProvider CreateServiceProvider(FakeServiceCollection containerBuilder) + { + containerBuilder.Build(); + return containerBuilder; + } + } +} diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeEveryService.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeEveryService.cs new file mode 100644 index 0000000000..3e472caf8a --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeEveryService.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.Hosting.Fakes +{ + interface IFakeEveryService : + IFakeScopedService, + IFakeServiceInstance, + IFakeSingletonService + { + } +} diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeScopedService.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeScopedService.cs new file mode 100644 index 0000000000..945a748c6c --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeScopedService.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.Extensions.Hosting.Fakes +{ + public interface IFakeScopedService : IFakeService + { + } +} diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeService.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeService.cs new file mode 100644 index 0000000000..b4489a6057 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeService.cs @@ -0,0 +1,7 @@ +// 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. + +namespace Microsoft.Extensions.Hosting.Fakes +{ + public interface IFakeService { } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeServiceInstance.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeServiceInstance.cs new file mode 100644 index 0000000000..479565bd1f --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeServiceInstance.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.Extensions.Hosting.Fakes +{ + interface IFakeServiceInstance : IFakeService + { + } +} diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeSingletonService.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeSingletonService.cs new file mode 100644 index 0000000000..ae6e7e79a4 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/IFakeSingletonService.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.Extensions.Hosting.Fakes +{ + interface IFakeSingletonService : IFakeService + { + } +} diff --git a/test/Microsoft.Extensions.Hosting.Tests/HostBuilderTests.cs b/test/Microsoft.Extensions.Hosting.Tests/HostBuilderTests.cs new file mode 100644 index 0000000000..0f567837be --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/HostBuilderTests.cs @@ -0,0 +1,459 @@ +// 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.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting.Fakes; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Hosting +{ + public class HostBuilderTests + { + [Fact] + public void ConfigureHostConfigurationPropagated() + { + var host = new HostBuilder() + .ConfigureHostConfiguration(configBuilder => + { + configBuilder.AddInMemoryCollection(new[] + { + new KeyValuePair("key1", "value1") + }); + }) + .ConfigureHostConfiguration(configBuilder => + { + configBuilder.AddInMemoryCollection(new[] + { + new KeyValuePair("key2", "value2") + }); + }) + .ConfigureHostConfiguration(configBuilder => + { + configBuilder.AddInMemoryCollection(new[] + { + // Hides value2 + new KeyValuePair("key2", "value3") + }); + }) + .ConfigureAppConfiguration((context, configBuilder) => + { + Assert.Equal("value1", context.Configuration["key1"]); + Assert.Equal("value3", context.Configuration["key2"]); + var config = configBuilder.Build(); + Assert.Equal("value1", config["key1"]); + Assert.Equal("value3", config["key2"]); + }) + .Build(); + + using (host) + { + var config = host.Services.GetRequiredService(); + Assert.Equal("value1", config["key1"]); + Assert.Equal("value3", config["key2"]); + } + } + + [Fact] + public void CanConfigureAppConfigurationAndRetrieveFromDI() + { + var hostBuilder = new HostBuilder() + .ConfigureAppConfiguration((_, configBuilder) => + { + configBuilder.AddInMemoryCollection( + new KeyValuePair[] + { + new KeyValuePair("key1", "value1") + }); + }) + .ConfigureAppConfiguration((_, configBuilder) => + { + configBuilder.AddInMemoryCollection( + new KeyValuePair[] + { + new KeyValuePair("key2", "value2") + }); + }) + .ConfigureAppConfiguration((_, configBuilder) => + { + configBuilder.AddInMemoryCollection( + new KeyValuePair[] + { + // Hides value2 + new KeyValuePair("key2", "value3") + }); + }); + + using (var host = hostBuilder.Build()) + { + var config = host.Services.GetService(); + Assert.NotNull(config); + Assert.Equal("value1", config["key1"]); + Assert.Equal("value3", config["key2"]); + } + } + + [Fact] + public void DefaultIHostingEnvironmentValues() + { + var hostBuilder = new HostBuilder() + .ConfigureAppConfiguration((hostContext, appConfig) => + { + var env = hostContext.HostingEnvironment; + Assert.Equal(EnvironmentName.Production, env.EnvironmentName); + Assert.Null(env.ApplicationName); + Assert.Equal(AppContext.BaseDirectory, env.ContentRootPath); + Assert.IsAssignableFrom(env.ContentRootFileProvider); + }); + + using (var host = hostBuilder.Build()) + { + var env = host.Services.GetRequiredService(); + Assert.Equal(EnvironmentName.Production, env.EnvironmentName); + Assert.Null(env.ApplicationName); + Assert.Equal(AppContext.BaseDirectory, env.ContentRootPath); + Assert.IsAssignableFrom(env.ContentRootFileProvider); + } + } + + [Fact] + public void ConfigBasedSettingsConfigBasedOverride() + { + var settings = new Dictionary + { + { HostDefaults.EnvironmentKey, "EnvA" } + }; + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(settings) + .Build(); + + var overrideSettings = new Dictionary + { + { HostDefaults.EnvironmentKey, "EnvB" } + }; + + var overrideConfig = new ConfigurationBuilder() + .AddInMemoryCollection(overrideSettings) + .Build(); + + var hostBuilder = new HostBuilder() + .ConfigureHostConfiguration(configBuilder => configBuilder.AddConfiguration(config)) + .ConfigureHostConfiguration(configBuilder => configBuilder.AddConfiguration(overrideConfig)); + + using (var host = hostBuilder.Build()) + { + Assert.Equal("EnvB", host.Services.GetRequiredService().EnvironmentName); + } + } + + [Fact] + public void UseEnvironmentIsNotOverriden() + { + var vals = new Dictionary + { + { "ENV", "Dev" }, + }; + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + + var expected = "MY_TEST_ENVIRONMENT"; + + + using (var host = new HostBuilder() + .ConfigureHostConfiguration(configBuilder => configBuilder.AddConfiguration(config)) + .UseEnvironment(expected) + .Build()) + { + Assert.Equal(expected, host.Services.GetService().EnvironmentName); + } + } + + [Fact] + public void BuildAndDispose() + { + using (var host = new HostBuilder() + .Build()) { } + } + + [Fact] + public void UseBasePathConfiguresBasePath() + { + var vals = new Dictionary + { + { "ENV", "Dev" }, + }; + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + + using (var host = new HostBuilder() + .ConfigureHostConfiguration(configBuilder => configBuilder.AddConfiguration(config)) + .UseContentRoot("/") + .Build()) + { + Assert.Equal("/", host.Services.GetService().ContentRootPath); + } + } + + [Fact] + public void HostConfigParametersReadCorrectly() + { + var parameters = new Dictionary() + { + { "applicationName", "MyProjectReference"}, + { "environment", EnvironmentName.Development}, + { "contentRoot", Path.GetFullPath(".") } + }; + + var host = new HostBuilder() + .ConfigureHostConfiguration(config => + { + config.AddInMemoryCollection(parameters); + }).Build(); + + var env = host.Services.GetRequiredService(); + + Assert.Equal("MyProjectReference", env.ApplicationName); + Assert.Equal(EnvironmentName.Development, env.EnvironmentName); + Assert.Equal(Path.GetFullPath("."), env.ContentRootPath); + } + + [Fact] + public void RelativeContentRootIsResolved() + { + using (var host = new HostBuilder() + .UseContentRoot("testroot") + .Build()) + { + var basePath = host.Services.GetRequiredService().ContentRootPath; + Assert.True(Path.IsPathRooted(basePath)); + Assert.EndsWith(Path.DirectorySeparatorChar + "testroot", basePath); + } + } + + [Fact] + public void DefaultContentRootIsApplicationBasePath() + { + using (var host = new HostBuilder() + .Build()) + { + var appBase = AppContext.BaseDirectory; + Assert.Equal(appBase, host.Services.GetService().ContentRootPath); + } + } + + [Fact] + public void DefaultServicesAreAvailable() + { + using (var host = new HostBuilder() + .Build()) + { + Assert.NotNull(host.Services.GetRequiredService()); + Assert.NotNull(host.Services.GetRequiredService()); + Assert.NotNull(host.Services.GetRequiredService()); + Assert.NotNull(host.Services.GetRequiredService()); + Assert.NotNull(host.Services.GetRequiredService()); + Assert.NotNull(host.Services.GetRequiredService>()); + } + } + + [Fact] + public void DefaultCreatesLoggerFactory() + { + var hostBuilder = new HostBuilder(); + + using (var host = hostBuilder.Build()) + { + Assert.NotNull(host.Services.GetService()); + } + } + + [Fact] + public void MultipleConfigureLoggingInvokedInOrder() + { + var callCount = 0; //Verify ordering + var hostBuilder = new HostBuilder() + .ConfigureLogging((hostContext, loggerFactory) => + { + Assert.Equal(0, callCount++); + }) + .ConfigureLogging((hostContext, loggerFactory) => + { + Assert.Equal(1, callCount++); + }); + + using (hostBuilder.Build()) + { + Assert.Equal(2, callCount); + } + } + + [Fact] + public void HostingContextContainsAppConfigurationDuringConfigureServices() + { + var hostBuilder = new HostBuilder() + .ConfigureAppConfiguration((context, configBuilder) => + configBuilder.AddInMemoryCollection( + new KeyValuePair[] + { + new KeyValuePair("key1", "value1") + })) + .ConfigureServices((context, factory) => + { + Assert.Equal("value1", context.Configuration["key1"]); + }); + + using (hostBuilder.Build()) { } + } + + [Fact] + public void ConfigureDefaultServiceProvider() + { + var hostBuilder = new HostBuilder() + .ConfigureServices((hostContext, s) => + { + s.AddTransient(); + s.AddScoped(); + }) + .UseServiceProviderFactory(new DefaultServiceProviderFactory(new ServiceProviderOptions() + { + ValidateScopes = true, + })); + var host = hostBuilder.Build(); + + Assert.Throws(() => { host.Services.GetRequiredService(); }); + } + + [Fact] + public void ConfigureCustomServiceProvider() + { + var hostBuilder = new HostBuilder() + .ConfigureServices((hostContext, s) => + { + s.AddTransient(); + s.AddScoped(); + }) + .UseServiceProviderFactory(new FakeServiceProviderFactory()) + .ConfigureContainer((hostContext, container) => + { + Assert.Null(container.State); + container.State = "1"; + }) + .ConfigureContainer((hostContext, container) => + { + Assert.Equal("1", container.State); + container.State = "2"; + }); + var host = hostBuilder.Build(); + var fakeServices = host.Services.GetRequiredService(); + Assert.Equal("2", fakeServices.State); + } + + [Fact] + public void CustomContainerTypeMismatchThrows() + { + var hostBuilder = new HostBuilder() + .ConfigureServices((hostContext, s) => + { + s.AddTransient(); + s.AddScoped(); + }) + .UseServiceProviderFactory(new FakeServiceProviderFactory()) + .ConfigureContainer((hostContext, container) => + { + }); + Assert.Throws(() => hostBuilder.Build()); + } + + [Fact] + public void HostingContextContainsAppConfigurationDuringConfigureLogging() + { + var hostBuilder = new HostBuilder() + .ConfigureAppConfiguration((context, configBuilder) => + configBuilder.AddInMemoryCollection( + new KeyValuePair[] + { + new KeyValuePair("key1", "value1") + })) + .ConfigureLogging((context, factory) => + { + Assert.Equal("value1", context.Configuration["key1"]); + }); + + using (hostBuilder.Build()) { } + } + + [Fact] + public void ConfigureServices_CanBeCalledMultipleTimes() + { + var callCount = 0; // Verify ordering + var hostBuilder = new HostBuilder() + .ConfigureServices((hostContext, services) => + { + Assert.Equal(0, callCount++); + services.AddTransient(); + }) + .ConfigureServices((hostContext, services) => + { + Assert.Equal(1, callCount++); + services.AddTransient(); + }); + + using (var host = hostBuilder.Build()) + { + Assert.Equal(2, callCount); + + Assert.NotNull(host.Services.GetRequiredService()); + Assert.NotNull(host.Services.GetRequiredService()); + } + } + + [Fact] + public void Build_DoesNotAllowBuildingMuiltipleTimes() + { + var builder = new HostBuilder(); + using (builder.Build()) + { + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("Build can only be called once.", ex.Message); + } + } + + [Fact] + public void SetsFullPathToContentRoot() + { + var host = new HostBuilder() + .ConfigureHostConfiguration(config => + { + config.AddInMemoryCollection(new[] + { + new KeyValuePair(HostDefaults.ContentRootKey, Path.GetFullPath(".")) + }); + }) + .Build(); + var env = host.Services.GetRequiredService(); + + Assert.Equal(Path.GetFullPath("."), env.ContentRootPath); + Assert.IsAssignableFrom(env.ContentRootFileProvider); + } + + private class ServiceC + { + public ServiceC(ServiceD serviceD) { } + } + + internal class ServiceD { } + + internal class ServiceA { } + + internal class ServiceB { } + } +} diff --git a/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs b/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs new file mode 100644 index 0000000000..bf6a5e0970 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs @@ -0,0 +1,936 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Fakes; +using Microsoft.Extensions.Hosting.Tests.Fakes; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Hosting +{ + public class HostTests + { + [Fact] + public async Task HostInjectsHostingEnvironment() + { + using (var host = CreateBuilder() + .UseEnvironment("WithHostingEnvironment") + .Build()) + { + await host.StartAsync(); + var env = host.Services.GetService(); + Assert.Equal("WithHostingEnvironment", env.EnvironmentName); + } + } + + [Fact] + public void CanCreateApplicationServicesWithAddedServices() + { + using (var host = CreateBuilder().ConfigureServices((hostContext, services) => services.AddSingleton()).Build()) + { + Assert.NotNull(host.Services.GetRequiredService()); + } + } + + [Fact] + public void EnvDefaultsToProductionIfNoConfig() + { + using (var host = CreateBuilder().Build()) + { + var env = host.Services.GetService(); + Assert.Equal(EnvironmentName.Production, env.EnvironmentName); + } + } + + [Fact] + public void EnvDefaultsToConfigValueIfSpecified() + { + var vals = new Dictionary + { + { "Environment", EnvironmentName.Staging } + }; + + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + + using (var host = CreateBuilder(config).Build()) + { + var env = host.Services.GetService(); + Assert.Equal(EnvironmentName.Staging, env.EnvironmentName); + } + } + + [Fact] + public async Task IsEnvironment_Extension_Is_Case_Insensitive() + { + using (var host = CreateBuilder().Build()) + { + await host.StartAsync(); + var env = host.Services.GetRequiredService(); + Assert.True(env.IsEnvironment(EnvironmentName.Production)); + Assert.True(env.IsEnvironment("producTion")); + } + } + + [Fact] + public void HostCanBeStarted() + { + FakeHostedService service; + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(); + }) + .Start()) + { + service = (FakeHostedService)host.Services.GetRequiredService(); + Assert.NotNull(host); + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(0, service.DisposeCount); + } + + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(1, service.DisposeCount); + } + + [Fact] + public async Task HostedServiceStartNotCalledIfHostNotStarted() + { + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService(); + lifetime.StopApplication(); + + var svc = (TestHostedService)host.Services.GetRequiredService(); + Assert.False(svc.StartCalled); + await host.StopAsync(); + Assert.False(svc.StopCalled); + host.Dispose(); + Assert.False(svc.StopCalled); + Assert.True(svc.DisposeCalled); + } + } + + [Fact] + public async Task HostCanBeStoppedWhenNotStarted() + { + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(); + }) + .Build()) + { + var svc = (TestHostedService)host.Services.GetRequiredService(); + Assert.False(svc.StartCalled); + await host.StopAsync(); + Assert.False(svc.StopCalled); + host.Dispose(); + Assert.False(svc.StopCalled); + Assert.True(svc.DisposeCalled); + } + } + + [Fact] + public async Task AppCrashesOnStartWhenFirstHostedServiceThrows() + { + bool[] events1 = null; + bool[] events2 = null; + + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + events1 = RegisterCallbacksThatThrow(services); + events2 = RegisterCallbacksThatThrow(services); + }) + .Build()) + { + await Assert.ThrowsAsync(() => host.StartAsync()); + Assert.True(events1[0]); + Assert.False(events2[0]); + host.Dispose(); + // Stopping + Assert.False(events1[1]); + Assert.False(events2[1]); + } + } + + [Fact] + public async Task StartCanBeCancelled() + { + var serviceStarting = new ManualResetEvent(false); + var startCancelled = new ManualResetEvent(false); + FakeHostedService service; + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(_ => new FakeHostedService() + { + StartAction = ct => + { + Assert.False(ct.IsCancellationRequested); + serviceStarting.Set(); + Assert.True(startCancelled.WaitOne(TimeSpan.FromSeconds(5))); + ct.ThrowIfCancellationRequested(); + } + }); + }) + .Build()) + { + var cts = new CancellationTokenSource(); + + var startTask = Task.Run(() => host.StartAsync(cts.Token)); + Assert.True(serviceStarting.WaitOne(TimeSpan.FromSeconds(5))); + cts.Cancel(); + startCancelled.Set(); + await Assert.ThrowsAsync(() => startTask); + + Assert.NotNull(host); + service = (FakeHostedService)host.Services.GetRequiredService(); + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(0, service.DisposeCount); + } + + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(1, service.DisposeCount); + } + + [Fact] + public async Task HostLifetimeOnStartedDelaysStart() + { + var serviceStarting = new ManualResetEvent(false); + var lifetimeStart = new ManualResetEvent(false); + var lifetimeContinue = new ManualResetEvent(false); + FakeHostedService service; + FakeHostLifetime lifetime; + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(_ => new FakeHostedService() + { + StartAction = ct => + { + serviceStarting.Set(); + } + }); + services.AddSingleton(_ => new FakeHostLifetime() + { + StartAction = (callback, state) => + { + lifetimeStart.Set(); + Assert.True(lifetimeContinue.WaitOne(TimeSpan.FromSeconds(5))); + callback(state); + } + }); + }) + .Build()) + { + var startTask = Task.Run(() => host.StartAsync()); + Assert.True(lifetimeStart.WaitOne(TimeSpan.FromSeconds(5))); + Assert.False(serviceStarting.WaitOne(0)); + + lifetimeContinue.Set(); + Assert.True(serviceStarting.WaitOne(TimeSpan.FromSeconds(5))); + + await startTask; + + service = (FakeHostedService)host.Services.GetRequiredService(); + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(0, service.DisposeCount); + + lifetime = (FakeHostLifetime)host.Services.GetRequiredService(); + Assert.Equal(1, lifetime.StartCount); + Assert.Equal(1, lifetime.StoppingCount); + Assert.Equal(0, lifetime.StopCount); + } + + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(1, service.DisposeCount); + + Assert.Equal(1, lifetime.StartCount); + Assert.Equal(1, lifetime.StoppingCount); + Assert.Equal(0, lifetime.StopCount); + } + + [Fact] + public async Task HostLifetimeOnStartedCanBeCancelled() + { + var serviceStarting = new ManualResetEvent(false); + var lifetimeStart = new ManualResetEvent(false); + var lifetimeContinue = new ManualResetEvent(false); + FakeHostedService service; + FakeHostLifetime lifetime; + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(_ => new FakeHostedService() + { + StartAction = ct => + { + serviceStarting.Set(); + } + }); + services.AddSingleton(_ => new FakeHostLifetime() + { + StartAction = (callback, state) => + { + lifetimeStart.Set(); + } + }); + }) + .Build()) + { + var cts = new CancellationTokenSource(); + + var startTask = Task.Run(() => host.StartAsync(cts.Token)); + + Assert.True(lifetimeStart.WaitOne(TimeSpan.FromSeconds(5))); + Assert.False(serviceStarting.WaitOne(0)); + + cts.Cancel(); + await Assert.ThrowsAsync(() => startTask); + Assert.False(serviceStarting.WaitOne(0)); + + lifetimeContinue.Set(); + Assert.False(serviceStarting.WaitOne(0)); + + Assert.NotNull(host); + service = (FakeHostedService)host.Services.GetRequiredService(); + Assert.Equal(0, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(0, service.DisposeCount); + + lifetime = (FakeHostLifetime)host.Services.GetRequiredService(); + Assert.Equal(1, lifetime.StartCount); + Assert.Equal(1, lifetime.StoppingCount); + Assert.Equal(0, lifetime.StopCount); + } + + Assert.Equal(0, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(1, service.DisposeCount); + + Assert.Equal(1, lifetime.StartCount); + Assert.Equal(1, lifetime.StoppingCount); + Assert.Equal(0, lifetime.StopCount); + } + + [Fact] + public async Task HostLifetimeOnStoppingTriggersIApplicationLifetime() + { + var lifetimeRegistered = new ManualResetEvent(false); + Action stoppingAction = null; + object stoppingState = null; + FakeHostedService service; + FakeHostLifetime lifetime; + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(); + services.AddSingleton(_ => new FakeHostLifetime() + { + StartAction = (callback, state) => callback(state), + StoppingAction = (callback, state) => + { + stoppingAction = callback; + stoppingState = state; + lifetimeRegistered.Set(); + } + }); + }) + .Build()) + { + await host.StartAsync(); + Assert.True(lifetimeRegistered.WaitOne(0)); + + var appLifetime = host.Services.GetRequiredService(); + + stoppingAction(stoppingState); + + Assert.True(appLifetime.ApplicationStopping.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); + + service = (FakeHostedService)host.Services.GetRequiredService(); + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(0, service.DisposeCount); + + lifetime = (FakeHostLifetime)host.Services.GetRequiredService(); + Assert.Equal(1, lifetime.StartCount); + Assert.Equal(1, lifetime.StoppingCount); + Assert.Equal(0, lifetime.StopCount); + } + + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(1, service.DisposeCount); + + Assert.Equal(1, lifetime.StartCount); + Assert.Equal(1, lifetime.StoppingCount); + Assert.Equal(0, lifetime.StopCount); + } + + [Fact] + public async Task HostStopAsyncCallsHostLifetimeStopAsync() + { + FakeHostedService service; + FakeHostLifetime lifetime; + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(); + services.AddSingleton(_ => new FakeHostLifetime() + { + StartAction = (callback, state) => callback(state), + }); + }) + .Build()) + { + await host.StartAsync(); + + service = (FakeHostedService)host.Services.GetRequiredService(); + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(0, service.DisposeCount); + + lifetime = (FakeHostLifetime)host.Services.GetRequiredService(); + Assert.Equal(1, lifetime.StartCount); + Assert.Equal(1, lifetime.StoppingCount); + Assert.Equal(0, lifetime.StopCount); + + await host.StopAsync(); + + Assert.Equal(1, service.StartCount); + Assert.Equal(1, service.StopCount); + Assert.Equal(0, service.DisposeCount); + + Assert.Equal(1, lifetime.StartCount); + Assert.Equal(1, lifetime.StoppingCount); + Assert.Equal(1, lifetime.StopCount); + } + + Assert.Equal(1, service.StartCount); + Assert.Equal(1, service.StopCount); + Assert.Equal(1, service.DisposeCount); + + Assert.Equal(1, lifetime.StartCount); + Assert.Equal(1, lifetime.StoppingCount); + Assert.Equal(1, lifetime.StopCount); + } + + [Fact] + public async Task HostShutsDownWhenTokenTriggers() + { + FakeHostedService service; + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => services.AddSingleton()) + .Build()) + { + var lifetime = host.Services.GetRequiredService(); + service = (FakeHostedService)host.Services.GetRequiredService(); + + var cts = new CancellationTokenSource(); + + var runInBackground = host.RunAsync(cts.Token); + + // Wait on the host to be started + lifetime.ApplicationStarted.WaitHandle.WaitOne(); + + Assert.Equal(1, service.StartCount); + Assert.Equal(0, service.StopCount); + Assert.Equal(0, service.DisposeCount); + + cts.Cancel(); + + // Wait on the host to shutdown + lifetime.ApplicationStopped.WaitHandle.WaitOne(); + + // Wait for RunAsync to finish to guarantee Disposal of Host + await runInBackground; + + Assert.Equal(1, service.StopCount); + Assert.Equal(1, service.DisposeCount); + } + Assert.Equal(1, service.DisposeCount); + } + + [Fact] + public async Task HostStopAsyncCanBeCancelledEarly() + { + var service = new Mock(); + service.Setup(s => s.StopAsync(It.IsAny())) + .Returns(token => + { + return Task.Run(() => + { + token.WaitHandle.WaitOne(); + }); + }); + + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(service.Object); + }) + .Build()) + { + await host.StartAsync(); + + var cts = new CancellationTokenSource(); + + var task = host.StopAsync(cts.Token); + cts.Cancel(); + + Assert.Equal(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(8)))); + } + } + + [Fact] + public void HostApplicationLifetimeEventsOrderedCorrectlyDuringShutdown() + { + using (var host = CreateBuilder() + .Build()) + { + var lifetime = host.Services.GetRequiredService(); + var applicationStartedEvent = new ManualResetEventSlim(false); + var applicationStoppingEvent = new ManualResetEventSlim(false); + var applicationStoppedEvent = new ManualResetEventSlim(false); + var applicationStartedCompletedBeforeApplicationStopping = false; + var applicationStoppingCompletedBeforeApplicationStopped = false; + var applicationStoppedCompletedBeforeRunCompleted = false; + + lifetime.ApplicationStarted.Register(() => + { + applicationStartedEvent.Set(); + }); + + lifetime.ApplicationStopping.Register(() => + { + // Check whether the applicationStartedEvent has been set + applicationStartedCompletedBeforeApplicationStopping = applicationStartedEvent.IsSet; + + // Simulate work. + Thread.Sleep(1000); + + applicationStoppingEvent.Set(); + }); + + lifetime.ApplicationStopped.Register(() => + { + // Check whether the applicationStoppingEvent has been set + applicationStoppingCompletedBeforeApplicationStopped = applicationStoppingEvent.IsSet; + applicationStoppedEvent.Set(); + }); + + var runHostAndVerifyApplicationStopped = Task.Run(async () => + { + await host.RunAsync(); + // Check whether the applicationStoppingEvent has been set + applicationStoppedCompletedBeforeRunCompleted = applicationStoppedEvent.IsSet; + }); + + // Wait until application has started to shut down the host + Assert.True(applicationStartedEvent.Wait(5000)); + + // Trigger host shutdown on a separate thread + Task.Run(() => lifetime.StopApplication()); + + // Wait for all events and host.Run() to complete + Assert.True(runHostAndVerifyApplicationStopped.Wait(5000)); + + // Verify Ordering + Assert.True(applicationStartedCompletedBeforeApplicationStopping); + Assert.True(applicationStoppingCompletedBeforeApplicationStopped); + Assert.True(applicationStoppedCompletedBeforeRunCompleted); + } + } + + [Fact] + public async Task HostDisposesServiceProvider() + { + using (var host = CreateBuilder() + .ConfigureServices((hostContext, s) => + { + s.AddTransient(); + s.AddSingleton(); + }) + .Build()) + { + await host.StartAsync(); + + var singleton = (FakeService)host.Services.GetService(); + var transient = (FakeService)host.Services.GetService(); + + Assert.False(singleton.Disposed); + Assert.False(transient.Disposed); + + await host.StopAsync(); + + Assert.False(singleton.Disposed); + Assert.False(transient.Disposed); + + host.Dispose(); + + Assert.True(singleton.Disposed); + Assert.True(transient.Disposed); + } + } + + [Fact] + public async Task HostNotifiesApplicationStarted() + { + using (var host = CreateBuilder() + .Build()) + { + var applicationLifetime = host.Services.GetService(); + + Assert.False(applicationLifetime.ApplicationStarted.IsCancellationRequested); + + await host.StartAsync(); + Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested); + } + } + + [Fact] + public async Task HostNotifiesAllIApplicationLifetimeCallbacksEvenIfTheyThrow() + { + using (var host = CreateBuilder() + .Build()) + { + var applicationLifetime = host.Services.GetService(); + + var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted); + var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); + var stopped = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopped); + + await host.StartAsync(); + Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested); + Assert.True(started.All(s => s)); + await host.StopAsync(); + Assert.True(stopping.All(s => s)); + host.Dispose(); + Assert.True(stopped.All(s => s)); + } + } + + [Fact] + public async Task HostStopApplicationDoesNotFireStopOnHostedService() + { + var stoppingCalls = 0; + var disposingCalls = 0; + + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + Action started = () => + { + }; + + Action stopping = () => + { + stoppingCalls++; + }; + + Action disposing = () => + { + disposingCalls++; + }; + + services.AddSingleton(_ => new DelegateHostedService(started, stopping, disposing)); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService(); + lifetime.StopApplication(); + + await host.StartAsync(); + + Assert.Equal(0, stoppingCalls); + Assert.Equal(0, disposingCalls); + } + Assert.Equal(0, stoppingCalls); + Assert.Equal(1, disposingCalls); + } + + [Fact] + public async Task HostedServiceCanInjectApplicationLifetime() + { + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService(); + lifetime.StopApplication(); + + await host.StartAsync(); + var svc = (TestHostedService)host.Services.GetRequiredService(); + Assert.True(svc.StartCalled); + + await host.StopAsync(); + Assert.True(svc.StopCalled); + } + } + + [Fact] + public async Task HostStopApplicationFiresStopOnHostedService() + { + var stoppingCalls = 0; + var startedCalls = 0; + var disposingCalls = 0; + + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + Action started = () => + { + startedCalls++; + }; + + Action stopping = () => + { + stoppingCalls++; + }; + + Action disposing = () => + { + disposingCalls++; + }; + + services.AddSingleton(_ => new DelegateHostedService(started, stopping, disposing)); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService(); + + Assert.Equal(0, startedCalls); + + await host.StartAsync(); + Assert.Equal(1, startedCalls); + Assert.Equal(0, stoppingCalls); + Assert.Equal(0, disposingCalls); + + await host.StopAsync(); + + Assert.Equal(1, startedCalls); + Assert.Equal(1, stoppingCalls); + Assert.Equal(0, disposingCalls); + + host.Dispose(); + + Assert.Equal(1, startedCalls); + Assert.Equal(1, stoppingCalls); + Assert.Equal(1, disposingCalls); + } + } + + [Fact] + public async Task HostDisposeApplicationDoesNotFireStopOnHostedService() + { + var stoppingCalls = 0; + var startedCalls = 0; + var disposingCalls = 0; + + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + Action started = () => + { + startedCalls++; + }; + + Action stopping = () => + { + stoppingCalls++; + }; + + Action disposing = () => + { + disposingCalls++; + }; + + services.AddSingleton(_ => new DelegateHostedService(started, stopping, disposing)); + }) + .Build()) + { + var lifetime = host.Services.GetRequiredService(); + + Assert.Equal(0, startedCalls); + await host.StartAsync(); + Assert.Equal(1, startedCalls); + Assert.Equal(0, stoppingCalls); + Assert.Equal(0, disposingCalls); + host.Dispose(); + + Assert.Equal(0, stoppingCalls); + Assert.Equal(1, disposingCalls); + } + } + + [Fact] + public async Task HostDoesNotNotifyIApplicationLifetimeCallbacksIfIHostedServicesThrow() + { + bool[] events1 = null; + bool[] events2 = null; + + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => + { + events1 = RegisterCallbacksThatThrow(services); + events2 = RegisterCallbacksThatThrow(services); + }) + .Build()) + { + var applicationLifetime = host.Services.GetService(); + + var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted); + var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); + + await Assert.ThrowsAsync(() => host.StartAsync()); + Assert.True(events1[0]); + Assert.False(events2[0]); + Assert.False(started.All(s => s)); + host.Dispose(); + Assert.False(events1[1]); + Assert.False(events2[1]); + Assert.False(stopping.All(s => s)); + } + } + + [Fact] + public async Task Host_InvokesConfigureServicesMethodsOnlyOnce() + { + int configureServicesCount = 0; + using (var host = CreateBuilder() + .ConfigureServices((hostContext, services) => configureServicesCount++) + .Build()) + { + Assert.Equal(1, configureServicesCount); + await host.StartAsync(); + var services = host.Services; + var services2 = host.Services; + Assert.Equal(1, configureServicesCount); + } + } + + private IHostBuilder CreateBuilder(IConfiguration config = null) + { + return new HostBuilder().ConfigureHostConfiguration(builder => builder.AddConfiguration(config ?? new ConfigurationBuilder().Build())); + } + + private static bool[] RegisterCallbacksThatThrow(IServiceCollection services) + { + bool[] events = new bool[2]; + + Action started = () => + { + events[0] = true; + throw new InvalidOperationException(); + }; + + Action stopping = () => + { + events[1] = true; + throw new InvalidOperationException(); + }; + + services.AddSingleton(new DelegateHostedService(started, stopping, () => { })); + + return events; + } + + private static bool[] RegisterCallbacksThatThrow(CancellationToken token) + { + var signals = new bool[3]; + for (int i = 0; i < signals.Length; i++) + { + token.Register(state => + { + signals[(int)state] = true; + throw new InvalidOperationException(); + }, i); + } + + return signals; + } + + private class TestHostedService : IHostedService, IDisposable + { + private readonly IApplicationLifetime _lifetime; + + public TestHostedService(IApplicationLifetime lifetime) + { + _lifetime = lifetime; + } + + public bool StartCalled { get; set; } + public bool StopCalled { get; set; } + public bool DisposeCalled { get; set; } + + public Task StartAsync(CancellationToken token) + { + StartCalled = true; + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken token) + { + StopCalled = true; + return Task.CompletedTask; + } + + public void Dispose() + { + DisposeCalled = true; + } + } + + private class DelegateHostedService : IHostedService, IDisposable + { + private readonly Action _started; + private readonly Action _stopping; + private readonly Action _disposing; + + public DelegateHostedService(Action started, Action stopping, Action disposing) + { + _started = started; + _stopping = stopping; + _disposing = disposing; + } + + public Task StartAsync(CancellationToken token) + { + _started(); + return Task.CompletedTask; + } + public Task StopAsync(CancellationToken token) + { + _stopping(); + return Task.CompletedTask; + } + + public void Dispose() => _disposing(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.Hosting.Tests/Microsoft.Extensions.Hosting.Tests.csproj b/test/Microsoft.Extensions.Hosting.Tests/Microsoft.Extensions.Hosting.Tests.csproj new file mode 100644 index 0000000000..90f00d3e22 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/Microsoft.Extensions.Hosting.Tests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.0;net461 + netcoreapp2.0 + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.Extensions.Hosting.Tests/testroot/readme.txt b/test/Microsoft.Extensions.Hosting.Tests/testroot/readme.txt new file mode 100644 index 0000000000..f8e5f07a30 --- /dev/null +++ b/test/Microsoft.Extensions.Hosting.Tests/testroot/readme.txt @@ -0,0 +1 @@ +This file exists to preserve the parent directory in the GIT repo. Git does not preserve empty directories. \ No newline at end of file