#1163 Implement the generic host

This commit is contained in:
Chris Ross (ASP.NET) 2017-08-28 16:22:27 -07:00
parent 8b30efbe75
commit ae9da9290e
67 changed files with 3585 additions and 10 deletions

View File

@ -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}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<StartupObject>GenericHostSample.ProgramHelloWorld</StartupObject>
<OutputType>Exe</OutputType>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Extensions.Hosting\Microsoft.Extensions.Hosting.csproj" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<Reference Include="System.ServiceProcess" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
namespace GenericHostSample
{
internal class MyContainer
{
}
}

View File

@ -0,0 +1,18 @@
using System;
using Microsoft.Extensions.DependencyInjection;
namespace GenericHostSample
{
internal class MyContainerFactory : IServiceProviderFactory<MyContainer>
{
public MyContainer CreateBuilder(IServiceCollection services)
{
return new MyContainer();
}
public IServiceProvider CreateServiceProvider(MyContainer containerBuilder)
{
throw new NotImplementedException();
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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<MyContainer>(new MyContainerFactory())
.ConfigureContainer<MyContainer>((hostContext, container) =>
{
})
.ConfigureAppConfiguration((hostContext, config) =>
{
config.AddEnvironmentVariables();
config.AddJsonFile("appsettings.json", optional: true);
})
.ConfigureServices((hostContext, services) =>
{
services.AddScoped<IHostedService, MyServiceA>();
services.AddScoped<IHostedService, MyServiceB>();
})
.Build();
}
public void Start()
{
_host.Start();
}
public async Task StopAsync()
{
await _host.StopAsync(TimeSpan.FromSeconds(5));
_host.Dispose();
}
}
}

View File

@ -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<MyContainer>(new MyContainerFactory())
.ConfigureContainer<MyContainer>((hostContext, container) =>
{
})
.ConfigureAppConfiguration((hostContext, config) =>
{
config.AddEnvironmentVariables();
config.AddJsonFile("appsettings.json", optional: true);
config.AddCommandLine(args);
})
.ConfigureServices((hostContext, services) =>
{
services.AddScoped<IHostedService, MyServiceA>();
services.AddScoped<IHostedService, MyServiceB>();
})
.Build();
var s = host.Services;
using (host)
{
Console.WriteLine("Starting!");
await host.StartAsync();
Console.WriteLine("Started! Press <enter> to stop.");
Console.ReadLine();
Console.WriteLine("Stopping!");
await host.StopAsync();
Console.WriteLine("Stopped!");
}
}
}
}

View File

@ -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<IHostedService, MyServiceA>();
services.AddScoped<IHostedService, MyServiceB>();
});
await builder.RunConsoleAsync();
}
}
}

View File

@ -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<IHostedService, MyServiceA>();
services.AddScoped<IHostedService, MyServiceB>();
});
await builder.RunAsServiceAsync();
}
}
}
#endif

View File

@ -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<IHostLifetime, ServiceBaseLifetime>());
}
public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
{
return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
}
}
public class ServiceBaseLifetime : ServiceBase, IHostLifetime
{
private Action<object> _startCallback;
private Action<object> _stopCallback;
private object _startState;
private object _stopState;
public void RegisterDelayStartCallback(Action<object> callback, object state)
{
_startCallback = callback ?? throw new ArgumentNullException(nameof(callback));
_startState = state ?? throw new ArgumentNullException(nameof(state));
Run(this);
}
public void RegisterStopCallback(Action<object> 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

View File

@ -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<TContext>(IHttpApplication<TContext> 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<IServer, FakeServer>());
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<LangVersion>latest</LangVersion>
<SignAssembly>true</SignAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Hosting\Microsoft.AspNetCore.Hosting.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Extensions.Hosting\Microsoft.Extensions.Hosting.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
</ItemGroup>
</Project>

View File

@ -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();
}
}
}

View File

@ -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<HostBuilderContext, IApplicationBuilder> configureApp)
{
return builder.ConfigureServices((bulderContext, services) =>
{
services.Configure<WebHostServiceOptions>(options =>
{
options.ConfigureApp = configureApp;
});
services.AddSingleton<IHostedService, WebHostService>();
var listener = new DiagnosticListener("Microsoft.AspNetCore");
services.AddSingleton<DiagnosticListener>(listener);
services.AddSingleton<DiagnosticSource>(listener);
services.AddTransient<IHttpContextFactory, HttpContextFactory>();
services.AddScoped<IMiddlewareFactory, MiddlewareFactory>();
// Conjure up a RequestServices
services.AddTransient<IStartupFilter, AutoRequestServicesStartupFilter>();
services.AddTransient<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>();
// Ensure object pooling is available everywhere.
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
});
}
}
}

View File

@ -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<WebHostServiceOptions> options, IServiceProvider services, HostBuilderContext hostBuilderContext, IServer server,
ILogger<WebHostService> 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<WebHostService> Logger { get; }
public DiagnosticListener DiagnosticListener { get; }
public IHttpContextFactory HttpContextFactory { get; }
public Task StartAsync(CancellationToken cancellationToken)
{
Server.Features.Get<IServerAddressesFeature>()?.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);
}
}
}

View File

@ -0,0 +1,11 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
namespace GenericWebHost
{
public class WebHostServiceOptions
{
public Action<HostBuilderContext, IApplicationBuilder> ConfigureApp { get; internal set; }
}
}

View File

@ -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;

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Hosting
CancellationToken ApplicationStopped { get; }
/// <summary>
/// Requests termination the current application.
/// Requests termination of the current application.
/// </summary>
void StopApplication();
}

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Hosting
public interface IHostingEnvironment
{
/// <summary>
/// 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.
/// </summary>
string EnvironmentName { get; set; }

View File

@ -15,10 +15,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
</Project>

View File

@ -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
{

View File

@ -10,7 +10,6 @@
<ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.Hosting.Abstractions\Microsoft.AspNetCore.Hosting.Abstractions.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Hosting.Server.Abstractions\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Hosting
}
/// <summary>
/// Adds a delegate for configuring the provided <see cref="LoggerFactory"/>. This may be called multiple times.
/// Adds a delegate for configuring the provided <see cref="ILoggingBuilder"/>. This may be called multiple times.
/// </summary>
/// <param name="hostBuilder">The <see cref="IWebHostBuilder" /> to configure.</param>
/// <param name="configureLogging">The delegate that configures the <see cref="ILoggingBuilder"/>.</param>

View File

@ -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
{
/// <summary>
/// Commonly used environment names.
/// </summary>
public static class EnvironmentName
{
public static readonly string Development = "Development";
public static readonly string Staging = "Staging";
public static readonly string Production = "Production";
}
}

View File

@ -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
{
/// <summary>
/// Context containing the common services on the <see cref="IHost" />. Some properties may be null until set by the <see cref="IHost" />.
/// </summary>
public class HostBuilderContext
{
public HostBuilderContext(IDictionary<object, object> properties)
{
Properties = properties ?? throw new System.ArgumentNullException(nameof(properties));
}
/// <summary>
/// The <see cref="IHostingEnvironment" /> initialized by the <see cref="IHost" />.
/// </summary>
public IHostingEnvironment HostingEnvironment { get; set; }
/// <summary>
/// The <see cref="IConfiguration" /> containing the merged configuration of the application and the <see cref="IHost" />.
/// </summary>
public IConfiguration Configuration { get; set; }
/// <summary>
/// A central location for sharing state between components during the host building process.
/// </summary>
public IDictionary<object, object> Properties { get; }
}
}

View File

@ -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
{
/// <summary>
/// Constants for HostBuilder configuration keys.
/// </summary>
public static class HostDefaults
{
/// <summary>
/// The configuration key used to set <see cref="IHostingEnvironment.ApplicationName"/>.
/// </summary>
public static readonly string ApplicationKey = "applicationName";
/// <summary>
/// The configuration key used to set <see cref="IHostingEnvironment.EnvironmentName"/>.
/// </summary>
public static readonly string EnvironmentKey = "environment";
/// <summary>
/// The configuration key used to set <see cref="IHostingEnvironment.ContentRootPath"/>
/// and <see cref="IHostingEnvironment.ContentRootFileProvider"/>.
/// </summary>
public static readonly string ContentRootKey = "contentRoot";
}
}

View File

@ -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
{
/// <summary>
/// Start the web host and listen on the specified urls.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to start.</param>
/// <returns>The <see cref="IHostBuilder"/>.</returns>
public static IHost Start(this IHostBuilder hostBuilder)
{
var host = hostBuilder.Build();
host.StartAsync(CancellationToken.None).GetAwaiter().GetResult();
return host;
}
}
}

View File

@ -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
{
/// <summary>
/// Starts the host synchronously.
/// </summary>
/// <param name="host"></param>
public static void Start(this IHost host)
{
host.StartAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Attempts to gracefully stop the host with the given timeout.
/// </summary>
/// <param name="host"></param>
/// <param name="timeout">The timeout for stopping gracefully. Once expired the
/// server may terminate any remaining active connections.</param>
/// <returns></returns>
public static Task StopAsync(this IHost host, TimeSpan timeout)
{
return host.StopAsync(new CancellationTokenSource(timeout).Token);
}
/// <summary>
/// Block the calling thread until shutdown is triggered via Ctrl+C or SIGTERM.
/// </summary>
/// <param name="host">The running <see cref="IHost"/>.</param>
public static void WaitForShutdown(this IHost host)
{
host.WaitForShutdownAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Runs a web application and block the calling thread until host shutdown.
/// </summary>
/// <param name="host">The <see cref="IHost"/> to run.</param>
public static void Run(this IHost host)
{
host.RunAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Runs a web application and returns a Task that only completes when the token is triggered or shutdown is triggered.
/// </summary>
/// <param name="host">The <see cref="IHost"/> to run.</param>
/// <param name="token">The token to trigger shutdown.</param>
public static async Task RunAsync(this IHost host, CancellationToken token = default(CancellationToken))
{
using (host)
{
await host.StartAsync(token);
await host.WaitForShutdownAsync(token);
}
}
/// <summary>
/// Returns a Task that completes when shutdown is triggered via the given token.
/// </summary>
/// <param name="host">The running <see cref="IHost"/>.</param>
/// <param name="token">The token to trigger shutdown.</param>
public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default(CancellationToken))
{
var applicationLifetime = host.Services.GetService<IApplicationLifetime>();
token.Register(state =>
{
((IApplicationLifetime)state).StopApplication();
},
applicationLifetime);
var waitForStop = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
applicationLifetime.ApplicationStopping.Register(obj =>
{
var tcs = (TaskCompletionSource<object>)obj;
tcs.TrySetResult(null);
}, waitForStop);
await waitForStop.Task;
// WebHost will use its default ShutdownTimeout if none is specified.
await host.StopAsync();
}
}
}

View File

@ -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
{
/// <summary>
/// Extension methods for <see cref="IHostingEnvironment"/>.
/// </summary>
public static class HostingEnvironmentExtensions
{
/// <summary>
/// Checks if the current hosting environment name is <see cref="EnvironmentName.Development"/>.
/// </summary>
/// <param name="hostingEnvironment">An instance of <see cref="IHostingEnvironment"/>.</param>
/// <returns>True if the environment name is <see cref="EnvironmentName.Development"/>, otherwise false.</returns>
public static bool IsDevelopment(this IHostingEnvironment hostingEnvironment)
{
if (hostingEnvironment == null)
{
throw new ArgumentNullException(nameof(hostingEnvironment));
}
return hostingEnvironment.IsEnvironment(EnvironmentName.Development);
}
/// <summary>
/// Checks if the current hosting environment name is <see cref="EnvironmentName.Staging"/>.
/// </summary>
/// <param name="hostingEnvironment">An instance of <see cref="IHostingEnvironment"/>.</param>
/// <returns>True if the environment name is <see cref="EnvironmentName.Staging"/>, otherwise false.</returns>
public static bool IsStaging(this IHostingEnvironment hostingEnvironment)
{
if (hostingEnvironment == null)
{
throw new ArgumentNullException(nameof(hostingEnvironment));
}
return hostingEnvironment.IsEnvironment(EnvironmentName.Staging);
}
/// <summary>
/// Checks if the current hosting environment name is <see cref="EnvironmentName.Production"/>.
/// </summary>
/// <param name="hostingEnvironment">An instance of <see cref="IHostingEnvironment"/>.</param>
/// <returns>True if the environment name is <see cref="EnvironmentName.Production"/>, otherwise false.</returns>
public static bool IsProduction(this IHostingEnvironment hostingEnvironment)
{
if (hostingEnvironment == null)
{
throw new ArgumentNullException(nameof(hostingEnvironment));
}
return hostingEnvironment.IsEnvironment(EnvironmentName.Production);
}
/// <summary>
/// Compares the current hosting environment name against the specified value.
/// </summary>
/// <param name="hostingEnvironment">An instance of <see cref="IHostingEnvironment"/>.</param>
/// <param name="environmentName">Environment name to validate against.</param>
/// <returns>True if the specified name is the same as the current environment, otherwise false.</returns>
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);
}
}
}

View File

@ -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
{
/// <summary>
/// Allows consumers to perform cleanup during a graceful shutdown.
/// </summary>
public interface IApplicationLifetime
{
/// <summary>
/// Triggered when the application host has fully started and is about to wait
/// for a graceful shutdown.
/// </summary>
CancellationToken ApplicationStarted { get; }
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// Requests may still be in flight. Shutdown will block until this event completes.
/// </summary>
CancellationToken ApplicationStopping { get; }
/// <summary>
/// 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.
/// </summary>
CancellationToken ApplicationStopped { get; }
/// <summary>
/// Requests termination of the current application.
/// </summary>
void StopApplication();
}
}

View File

@ -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
{
/// <summary>
/// A program abstraction.
/// </summary>
public interface IHost : IDisposable
{
/// <summary>
/// The programs configured services.
/// </summary>
IServiceProvider Services { get; }
/// <summary>
/// Start the program.
/// </summary>
/// <param name="cancellationToken">Used to abort program start.</param>
/// <returns></returns>
Task StartAsync(CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Attempts to gracefully stop the program.
/// </summary>
/// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
/// <returns></returns>
Task StopAsync(CancellationToken cancellationToken = default(CancellationToken));
}
}

View File

@ -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
{
/// <summary>
/// A program initialization abstraction.
/// </summary>
public interface IHostBuilder
{
/// <summary>
/// A central location for sharing state between components during the host building process.
/// </summary>
IDictionary<object, object> Properties { get; }
/// <summary>
/// Set up the configuration for the builder itself. This will be used to initialize the <see cref="IHostingEnvironment"/>
/// for use later in the build process. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used
/// to construct the <see cref="IConfiguration"/> for the host.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate);
/// <summary>
/// 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 <see cref="HostBuilderContext.Configuration"/> for
/// subsequent operations, as well as in <see cref="IHost.Services"/>.
/// </summary>
/// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used
/// to construct the <see cref="IConfiguration"/> for the application.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate);
/// <summary>
/// Adds services to the container. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate">The delegate for configuring the <see cref="IServiceCollection"/> that will be used
/// to construct the <see cref="IServiceProvider"/>.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate);
/// <summary>
/// Overrides the factory used to create the service provider.
/// </summary>
/// <typeparam name="TContainerBuilder"></typeparam>
/// <param name="factory"></param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory);
/// <summary>
/// Enables configuring the instantiated dependency container. This can be called multiple times and
/// the results will be additive.
/// </summary>
/// <typeparam name="TContainerBuilder"></typeparam>
/// <param name="configureDelegate"></param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate);
/// <summary>
/// Run the given actions to initialize the host. This can only be called once.
/// </summary>
/// <returns>An initialized <see cref="IHost"/></returns>
IHost Build();
}
}

View File

@ -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
{
/// <summary>
/// Called at the start of <see cref="IHost.StartAsync(CancellationToken)"/> which will wait until the callback is invoked before
/// continuing. This can be used to delay startup until signaled by an external event.
/// </summary>
/// <param name="callback">A callback that will be invoked when the host should continue.</param>
/// <param name="state">State to pass to the callback.</param>
void RegisterDelayStartCallback(Action<object> callback, object state);
/// <summary>
/// Called at the start of <see cref="IHost.StartAsync(CancellationToken)"/> to register the given callback for initiating the
/// application shutdown process.
/// </summary>
/// <param name="callback">A callback to invoke when an external signal indicates the application should stop.</param>
/// <param name="state">State to pass to the callback.</param>
void RegisterStopCallback(Action<object> callback, object state);
/// <summary>
/// Called from <see cref="IHost.StopAsync(CancellationToken)"/> to indicate that the host as stopped and clean up resources.
/// </summary>
/// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
/// <returns></returns>
Task StopAsync(CancellationToken cancellationToken);
}
}

View File

@ -14,11 +14,13 @@ namespace Microsoft.Extensions.Hosting
/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
Task StartAsync(CancellationToken cancellationToken);
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
Task StopAsync(CancellationToken cancellationToken);
}
}

View File

@ -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
{
/// <summary>
/// Provides information about the web hosting environment an application is running in.
/// </summary>
public interface IHostingEnvironment
{
/// <summary>
/// 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.
/// </summary>
string EnvironmentName { get; set; }
/// <summary>
/// Gets or sets the name of the application. This property is automatically set by the host to the assembly containing
/// the application entry point.
/// </summary>
string ApplicationName { get; set; }
/// <summary>
/// Gets or sets the absolute path to the directory that contains the application content files.
/// </summary>
string ContentRootPath { get; set; }
/// <summary>
/// Gets or sets an <see cref="IFileProvider"/> pointing at <see cref="ContentRootPath"/>.
/// </summary>
IFileProvider ContentRootFileProvider { get; set; }
}
}

View File

@ -7,6 +7,14 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>hosting</PackageTags>
<EnableApiCheck>false</EnableApiCheck>
<RootNamespace>Microsoft.Extensions.Hosting</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
</Project>

View File

@ -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
{
/// <summary>
/// Indicates if host lifetime status messages should be written to the console such as on startup.
/// The default is true.
/// </summary>
public bool WriteStatusMessages { get; set; } = true;
}
}

View File

@ -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
{
/// <summary>
/// A program initialization utility.
/// </summary>
public class HostBuilder : IHostBuilder
{
private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>();
private List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new List<Action<HostBuilderContext, IServiceCollection>>();
private List<IConfigureContainerAdapter> _configureContainerActions = new List<IConfigureContainerAdapter>();
private IServiceFactoryAdapter _serviceProviderFactory = new ServiceFactoryAdapter<IServiceCollection>(new DefaultServiceProviderFactory());
private bool _hostBuilt;
private IConfiguration _hostConfiguration;
private IConfiguration _appConfiguration;
private HostBuilderContext _hostBuilderContext;
private IHostingEnvironment _hostingEnvironment;
private IServiceProvider _appServices;
/// <summary>
/// A central location for sharing state between components during the host building process.
/// </summary>
public IDictionary<object, object> Properties => new Dictionary<object, object>();
/// <summary>
/// Set up the configuration for the builder itself. This will be used to initialize the <see cref="IHostingEnvironment"/>
/// for use later in the build process. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate"></param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
{
_configureHostConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
/// <summary>
/// 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 <see cref="HostBuilderContext.Configuration"/> for
/// subsequent operations, as well as in <see cref="IHost.Services"/>.
/// </summary>
/// <param name="configureDelegate"></param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
{
_configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
/// <summary>
/// Adds services to the container. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate"></param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
{
_configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
/// <summary>
/// Overrides the factory used to create the service provider.
/// </summary>
/// <typeparam name="TContainerBuilder"></typeparam>
/// <param name="factory"></param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory)
{
_serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(factory ?? throw new ArgumentNullException(nameof(factory)));
return this;
}
/// <summary>
/// Enables configuring the instantiated dependency container. This can be called multiple times and
/// the results will be additive.
/// </summary>
/// <typeparam name="TContainerBuilder"></typeparam>
/// <param name="configureDelegate"></param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate)
{
_configureContainerActions.Add(new ConfigureContainerAdapter<TContainerBuilder>(configureDelegate
?? throw new ArgumentNullException(nameof(configureDelegate))));
return this;
}
/// <summary>
/// Run the given actions to initialize the host. This can only be called once.
/// </summary>
/// <returns>An initialized <see cref="IHost"/></returns>
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<IApplicationLifetime, ApplicationLifetime>();
services.AddSingleton<IHostLifetime, HostProcessLifetime>();
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.");
}
}
}
}

View File

@ -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
{
/// <summary>
/// Specify the environment to be used by the web host.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to configure.</param>
/// <param name="environment">The environment to host the application in.</param>
/// <returns>The <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder UseEnvironment(this IHostBuilder hostBuilder, string environment)
{
return hostBuilder.ConfigureHostConfiguration(configBuilder =>
{
configBuilder.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>(HostDefaults.EnvironmentKey,
environment ?? throw new ArgumentNullException(nameof(environment)))
});
});
}
/// <summary>
/// Specify the content root directory to be used by the web host.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to configure.</param>
/// <param name="contentRoot">Path to root directory of the application.</param>
/// <returns>The <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder UseContentRoot(this IHostBuilder hostBuilder, string contentRoot)
{
return hostBuilder.ConfigureHostConfiguration(configBuilder =>
{
configBuilder.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>(HostDefaults.ContentRootKey,
contentRoot ?? throw new ArgumentNullException(nameof(contentRoot)))
});
});
}
/// <summary>
/// Adds a delegate for configuring the provided <see cref="ILoggingBuilder"/>. This may be called multiple times.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <param name="configureLogging">The delegate that configures the <see cref="ILoggingBuilder"/>.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder, Action<HostBuilderContext, ILoggingBuilder> configureLogging)
{
return hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder)));
}
/// <summary>
/// Listens for Ctrl+C or SIGTERM and calls <see cref="IApplicationLifetime.StopApplication"/> to start the shutdown process.
/// This will unblock extensions like RunAsync and WaitForShutdownAsync.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton<IHostLifetime, ConsoleLifetime>());
}
/// <summary>
/// Enables console support, builds and starts the host, and waits for Ctrl+C or SIGTERM to shut down.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default(CancellationToken))
{
return hostBuilder.UseConsoleLifetime().Build().RunAsync(cancellationToken);
}
}
}

View File

@ -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
{
/// <summary>
/// Allows consumers to perform cleanup during a graceful shutdown.
/// </summary>
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<ApplicationLifetime> _logger;
public ApplicationLifetime(ILogger<ApplicationLifetime> logger)
{
_logger = logger;
}
/// <summary>
/// Triggered when the application host has fully started and is about to wait
/// for a graceful shutdown.
/// </summary>
public CancellationToken ApplicationStarted => _startedSource.Token;
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// Request may still be in flight. Shutdown will block until this event completes.
/// </summary>
public CancellationToken ApplicationStopping => _stoppingSource.Token;
/// <summary>
/// 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.
/// </summary>
public CancellationToken ApplicationStopped => _stoppedSource.Token;
/// <summary>
/// Signals the ApplicationStopping event and blocks until it completes.
/// </summary>
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);
}
}
}
/// <summary>
/// Signals the ApplicationStarted event and blocks until it completes.
/// </summary>
public void NotifyStarted()
{
try
{
ExecuteHandlers(_startedSource);
}
catch (Exception ex)
{
_logger.ApplicationError(LoggerEventIds.ApplicationStartupException,
"An error occurred starting the application",
ex);
}
}
/// <summary>
/// Signals the ApplicationStopped event and blocks until it completes.
/// </summary>
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<Exception> exceptions = null;
try
{
// Run the cancellation token callbacks
cancel.Cancel(throwOnFirstException: false);
}
catch (Exception ex)
{
if (exceptions == null)
{
exceptions = new List<Exception>();
}
exceptions.Add(ex);
}
// Throw an aggregate exception if there were any exceptions
if (exceptions != null)
{
throw new AggregateException(exceptions);
}
}
}
}

View File

@ -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<TContainerBuilder> : IConfigureContainerAdapter
{
private Action<HostBuilderContext, TContainerBuilder> _action;
public ConfigureContainerAdapter(Action<HostBuilderContext, TContainerBuilder> action)
{
_action = action ?? throw new ArgumentNullException(nameof(action));
}
public void ConfigureContainer(HostBuilderContext hostContext, object containerBuilder)
{
_action(hostContext, (TContainerBuilder)containerBuilder);
}
}
}

View File

@ -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
{
/// <summary>
/// Listens for Ctrl+C or SIGTERM and initiates shutdown.
/// </summary>
public class ConsoleLifetime : IHostLifetime
{
public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> 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<object> 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<object> 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;
}
}
}

View File

@ -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<Host> _logger;
private IHostLifetime _hostLifetime;
private ApplicationLifetime _applicationLifetime;
private IEnumerable<IHostedService> _hostedServices;
internal Host(IServiceProvider services)
{
Services = services ?? throw new ArgumentNullException(nameof(services));
_applicationLifetime = Services.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
_logger = Services.GetRequiredService<ILogger<Host>>();
_hostLifetime = Services.GetRequiredService<IHostLifetime>();
}
public IServiceProvider Services { get; }
public async Task StartAsync(CancellationToken cancellationToken = default(CancellationToken))
{
_logger.Starting();
var delayStart = new TaskCompletionSource<object>();
cancellationToken.Register(obj => ((TaskCompletionSource<object>)obj).TrySetCanceled(), delayStart);
_hostLifetime.RegisterDelayStartCallback(obj => ((TaskCompletionSource<object>)obj).TrySetResult(null), delayStart);
_hostLifetime.RegisterStopCallback(obj => (obj as IApplicationLifetime)?.StopApplication(), _applicationLifetime);
await delayStart.Task;
_hostedServices = Services.GetService<IEnumerable<IHostedService>>();
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<Exception> exceptions = new List<Exception>();
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();
}
}
}

View File

@ -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<object> callback, object state)
{
// Never delays start.
callback(state);
}
public void RegisterStopCallback(Action<object> callback, object state)
{
AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => callback(state);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@ -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; }
}
}

View File

@ -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");
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<TContainerBuilder> : IServiceFactoryAdapter
{
private IServiceProviderFactory<TContainerBuilder> _serviceProviderFactory;
public ServiceFactoryAdapter(IServiceProviderFactory<TContainerBuilder> 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);
}
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>.NET Core hosting and startup infrastructures for applications.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>hosting</PackageTags>
<EnableApiCheck>false</EnableApiCheck>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Extensions.Hosting.Abstractions\Microsoft.Extensions.Hosting.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" />
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>
</Project>

View File

@ -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<Action<object>, object> StartAction { get; set; }
public Action<Action<object>, object> StoppingAction { get; set; }
public Action StopAction { get; set; }
public void RegisterDelayStartCallback(Action<object> callback, object state)
{
StartCount++;
StartAction?.Invoke(callback, state);
}
public void RegisterStopCallback(Action<object> callback, object state)
{
StoppingCount++;
StoppingAction?.Invoke(callback, state);
}
public Task StopAsync(CancellationToken cancellationToken)
{
StopCount++;
StopAction?.Invoke();
return Task.CompletedTask;
}
}
}

View File

@ -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<CancellationToken> StartAction { get; set; }
public Action<CancellationToken> 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();
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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<FakeServiceCollection>(this);
}
public void Build()
{
_inner = _services.BuildServiceProvider();
}
public void MyFancyContainerMethod()
{
FancyMethodCalled = true;
}
}
}

View File

@ -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<FakeServiceCollection>
{
public FakeServiceCollection CreateBuilder(IServiceCollection services)
{
var container = new FakeServiceCollection();
container.Populate(services);
return container;
}
public IServiceProvider CreateServiceProvider(FakeServiceCollection containerBuilder)
{
containerBuilder.Build();
return containerBuilder;
}
}
}

View File

@ -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
{
}
}

View File

@ -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
{
}
}

View File

@ -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 { }
}

View File

@ -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
{
}
}

View File

@ -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
{
}
}

View File

@ -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<string, string>("key1", "value1")
});
})
.ConfigureHostConfiguration(configBuilder =>
{
configBuilder.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("key2", "value2")
});
})
.ConfigureHostConfiguration(configBuilder =>
{
configBuilder.AddInMemoryCollection(new[]
{
// Hides value2
new KeyValuePair<string, string>("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<IConfiguration>();
Assert.Equal("value1", config["key1"]);
Assert.Equal("value3", config["key2"]);
}
}
[Fact]
public void CanConfigureAppConfigurationAndRetrieveFromDI()
{
var hostBuilder = new HostBuilder()
.ConfigureAppConfiguration((_, configBuilder) =>
{
configBuilder.AddInMemoryCollection(
new KeyValuePair<string, string>[]
{
new KeyValuePair<string, string>("key1", "value1")
});
})
.ConfigureAppConfiguration((_, configBuilder) =>
{
configBuilder.AddInMemoryCollection(
new KeyValuePair<string, string>[]
{
new KeyValuePair<string, string>("key2", "value2")
});
})
.ConfigureAppConfiguration((_, configBuilder) =>
{
configBuilder.AddInMemoryCollection(
new KeyValuePair<string, string>[]
{
// Hides value2
new KeyValuePair<string, string>("key2", "value3")
});
});
using (var host = hostBuilder.Build())
{
var config = host.Services.GetService<IConfiguration>();
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<PhysicalFileProvider>(env.ContentRootFileProvider);
});
using (var host = hostBuilder.Build())
{
var env = host.Services.GetRequiredService<IHostingEnvironment>();
Assert.Equal(EnvironmentName.Production, env.EnvironmentName);
Assert.Null(env.ApplicationName);
Assert.Equal(AppContext.BaseDirectory, env.ContentRootPath);
Assert.IsAssignableFrom<PhysicalFileProvider>(env.ContentRootFileProvider);
}
}
[Fact]
public void ConfigBasedSettingsConfigBasedOverride()
{
var settings = new Dictionary<string, string>
{
{ HostDefaults.EnvironmentKey, "EnvA" }
};
var config = new ConfigurationBuilder()
.AddInMemoryCollection(settings)
.Build();
var overrideSettings = new Dictionary<string, string>
{
{ 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<IHostingEnvironment>().EnvironmentName);
}
}
[Fact]
public void UseEnvironmentIsNotOverriden()
{
var vals = new Dictionary<string, string>
{
{ "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<IHostingEnvironment>().EnvironmentName);
}
}
[Fact]
public void BuildAndDispose()
{
using (var host = new HostBuilder()
.Build()) { }
}
[Fact]
public void UseBasePathConfiguresBasePath()
{
var vals = new Dictionary<string, string>
{
{ "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<IHostingEnvironment>().ContentRootPath);
}
}
[Fact]
public void HostConfigParametersReadCorrectly()
{
var parameters = new Dictionary<string, string>()
{
{ "applicationName", "MyProjectReference"},
{ "environment", EnvironmentName.Development},
{ "contentRoot", Path.GetFullPath(".") }
};
var host = new HostBuilder()
.ConfigureHostConfiguration(config =>
{
config.AddInMemoryCollection(parameters);
}).Build();
var env = host.Services.GetRequiredService<IHostingEnvironment>();
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<IHostingEnvironment>().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<IHostingEnvironment>().ContentRootPath);
}
}
[Fact]
public void DefaultServicesAreAvailable()
{
using (var host = new HostBuilder()
.Build())
{
Assert.NotNull(host.Services.GetRequiredService<IHostingEnvironment>());
Assert.NotNull(host.Services.GetRequiredService<IConfiguration>());
Assert.NotNull(host.Services.GetRequiredService<HostBuilderContext>());
Assert.NotNull(host.Services.GetRequiredService<IApplicationLifetime>());
Assert.NotNull(host.Services.GetRequiredService<ILoggerFactory>());
Assert.NotNull(host.Services.GetRequiredService<IOptions<FakeOptions>>());
}
}
[Fact]
public void DefaultCreatesLoggerFactory()
{
var hostBuilder = new HostBuilder();
using (var host = hostBuilder.Build())
{
Assert.NotNull(host.Services.GetService<ILoggerFactory>());
}
}
[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<string, string>[]
{
new KeyValuePair<string, string>("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<ServiceD>();
s.AddScoped<ServiceC>();
})
.UseServiceProviderFactory(new DefaultServiceProviderFactory(new ServiceProviderOptions()
{
ValidateScopes = true,
}));
var host = hostBuilder.Build();
Assert.Throws<InvalidOperationException>(() => { host.Services.GetRequiredService<ServiceC>(); });
}
[Fact]
public void ConfigureCustomServiceProvider()
{
var hostBuilder = new HostBuilder()
.ConfigureServices((hostContext, s) =>
{
s.AddTransient<ServiceD>();
s.AddScoped<ServiceC>();
})
.UseServiceProviderFactory(new FakeServiceProviderFactory())
.ConfigureContainer<FakeServiceCollection>((hostContext, container) =>
{
Assert.Null(container.State);
container.State = "1";
})
.ConfigureContainer<FakeServiceCollection>((hostContext, container) =>
{
Assert.Equal("1", container.State);
container.State = "2";
});
var host = hostBuilder.Build();
var fakeServices = host.Services.GetRequiredService<FakeServiceCollection>();
Assert.Equal("2", fakeServices.State);
}
[Fact]
public void CustomContainerTypeMismatchThrows()
{
var hostBuilder = new HostBuilder()
.ConfigureServices((hostContext, s) =>
{
s.AddTransient<ServiceD>();
s.AddScoped<ServiceC>();
})
.UseServiceProviderFactory(new FakeServiceProviderFactory())
.ConfigureContainer<IServiceCollection>((hostContext, container) =>
{
});
Assert.Throws<InvalidCastException>(() => hostBuilder.Build());
}
[Fact]
public void HostingContextContainsAppConfigurationDuringConfigureLogging()
{
var hostBuilder = new HostBuilder()
.ConfigureAppConfiguration((context, configBuilder) =>
configBuilder.AddInMemoryCollection(
new KeyValuePair<string, string>[]
{
new KeyValuePair<string, string>("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<ServiceA>();
})
.ConfigureServices((hostContext, services) =>
{
Assert.Equal(1, callCount++);
services.AddTransient<ServiceB>();
});
using (var host = hostBuilder.Build())
{
Assert.Equal(2, callCount);
Assert.NotNull(host.Services.GetRequiredService<ServiceA>());
Assert.NotNull(host.Services.GetRequiredService<ServiceB>());
}
}
[Fact]
public void Build_DoesNotAllowBuildingMuiltipleTimes()
{
var builder = new HostBuilder();
using (builder.Build())
{
var ex = Assert.Throws<InvalidOperationException>(() => 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<string, string>(HostDefaults.ContentRootKey, Path.GetFullPath("."))
});
})
.Build();
var env = host.Services.GetRequiredService<IHostingEnvironment>();
Assert.Equal(Path.GetFullPath("."), env.ContentRootPath);
Assert.IsAssignableFrom<PhysicalFileProvider>(env.ContentRootFileProvider);
}
private class ServiceC
{
public ServiceC(ServiceD serviceD) { }
}
internal class ServiceD { }
internal class ServiceA { }
internal class ServiceB { }
}
}

View File

@ -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<IHostingEnvironment>();
Assert.Equal("WithHostingEnvironment", env.EnvironmentName);
}
}
[Fact]
public void CanCreateApplicationServicesWithAddedServices()
{
using (var host = CreateBuilder().ConfigureServices((hostContext, services) => services.AddSingleton<IFakeService, FakeService>()).Build())
{
Assert.NotNull(host.Services.GetRequiredService<IFakeService>());
}
}
[Fact]
public void EnvDefaultsToProductionIfNoConfig()
{
using (var host = CreateBuilder().Build())
{
var env = host.Services.GetService<IHostingEnvironment>();
Assert.Equal(EnvironmentName.Production, env.EnvironmentName);
}
}
[Fact]
public void EnvDefaultsToConfigValueIfSpecified()
{
var vals = new Dictionary<string, string>
{
{ "Environment", EnvironmentName.Staging }
};
var builder = new ConfigurationBuilder()
.AddInMemoryCollection(vals);
var config = builder.Build();
using (var host = CreateBuilder(config).Build())
{
var env = host.Services.GetService<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostedService, FakeHostedService>();
})
.Start())
{
service = (FakeHostedService)host.Services.GetRequiredService<IHostedService>();
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<IHostedService, TestHostedService>();
})
.Build())
{
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
lifetime.StopApplication();
var svc = (TestHostedService)host.Services.GetRequiredService<IHostedService>();
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<IHostedService, TestHostedService>();
})
.Build())
{
var svc = (TestHostedService)host.Services.GetRequiredService<IHostedService>();
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<InvalidOperationException>(() => 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<IHostedService>(_ => 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<OperationCanceledException>(() => startTask);
Assert.NotNull(host);
service = (FakeHostedService)host.Services.GetRequiredService<IHostedService>();
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<IHostedService>(_ => new FakeHostedService()
{
StartAction = ct =>
{
serviceStarting.Set();
}
});
services.AddSingleton<IHostLifetime>(_ => 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<IHostedService>();
Assert.Equal(1, service.StartCount);
Assert.Equal(0, service.StopCount);
Assert.Equal(0, service.DisposeCount);
lifetime = (FakeHostLifetime)host.Services.GetRequiredService<IHostLifetime>();
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<IHostedService>(_ => new FakeHostedService()
{
StartAction = ct =>
{
serviceStarting.Set();
}
});
services.AddSingleton<IHostLifetime>(_ => 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<TaskCanceledException>(() => startTask);
Assert.False(serviceStarting.WaitOne(0));
lifetimeContinue.Set();
Assert.False(serviceStarting.WaitOne(0));
Assert.NotNull(host);
service = (FakeHostedService)host.Services.GetRequiredService<IHostedService>();
Assert.Equal(0, service.StartCount);
Assert.Equal(0, service.StopCount);
Assert.Equal(0, service.DisposeCount);
lifetime = (FakeHostLifetime)host.Services.GetRequiredService<IHostLifetime>();
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<object> stoppingAction = null;
object stoppingState = null;
FakeHostedService service;
FakeHostLifetime lifetime;
using (var host = CreateBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddSingleton<IHostedService, FakeHostedService>();
services.AddSingleton<IHostLifetime>(_ => 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<IApplicationLifetime>();
stoppingAction(stoppingState);
Assert.True(appLifetime.ApplicationStopping.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
service = (FakeHostedService)host.Services.GetRequiredService<IHostedService>();
Assert.Equal(1, service.StartCount);
Assert.Equal(0, service.StopCount);
Assert.Equal(0, service.DisposeCount);
lifetime = (FakeHostLifetime)host.Services.GetRequiredService<IHostLifetime>();
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<IHostedService, FakeHostedService>();
services.AddSingleton<IHostLifetime>(_ => new FakeHostLifetime()
{
StartAction = (callback, state) => callback(state),
});
})
.Build())
{
await host.StartAsync();
service = (FakeHostedService)host.Services.GetRequiredService<IHostedService>();
Assert.Equal(1, service.StartCount);
Assert.Equal(0, service.StopCount);
Assert.Equal(0, service.DisposeCount);
lifetime = (FakeHostLifetime)host.Services.GetRequiredService<IHostLifetime>();
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<IHostedService, FakeHostedService>())
.Build())
{
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
service = (FakeHostedService)host.Services.GetRequiredService<IHostedService>();
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<IHostedService>();
service.Setup(s => s.StopAsync(It.IsAny<CancellationToken>()))
.Returns<CancellationToken>(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<IApplicationLifetime>();
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<IFakeService, FakeService>();
s.AddSingleton<IFakeSingletonService, FakeService>();
})
.Build())
{
await host.StartAsync();
var singleton = (FakeService)host.Services.GetService<IFakeSingletonService>();
var transient = (FakeService)host.Services.GetService<IFakeService>();
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<IApplicationLifetime>();
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<IApplicationLifetime>();
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<IHostedService>(_ => new DelegateHostedService(started, stopping, disposing));
})
.Build())
{
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
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<IHostedService, TestHostedService>();
})
.Build())
{
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
lifetime.StopApplication();
await host.StartAsync();
var svc = (TestHostedService)host.Services.GetRequiredService<IHostedService>();
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<IHostedService>(_ => new DelegateHostedService(started, stopping, disposing));
})
.Build())
{
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
Assert.Equal(0, startedCalls);
await host.StartAsync();
Assert.Equal(1, startedCalls);
Assert.Equal(0, stoppingCalls);
Assert.Equal(0, disposingCalls);
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<IHostedService>(_ => new DelegateHostedService(started, stopping, disposing));
})
.Build())
{
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
Assert.Equal(0, startedCalls);
await host.StartAsync();
Assert.Equal(1, startedCalls);
Assert.Equal(0, stoppingCalls);
Assert.Equal(0, disposingCalls);
host.Dispose();
Assert.Equal(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<IApplicationLifetime>();
var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted);
var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
await Assert.ThrowsAsync<InvalidOperationException>(() => 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<IHostedService>(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();
}
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Content Include="testroot\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Extensions.Hosting\Microsoft.Extensions.Hosting.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Logging.Testing" />
<PackageReference Include="Microsoft.Extensions.Options" />
</ItemGroup>
</Project>

View File

@ -0,0 +1 @@
This file exists to preserve the parent directory in the GIT repo. Git does not preserve empty directories.