Host,官方機翻叫做主機,是一個封裝了應用程式資源以及生命週期的物件,包含了
管理應用程式生命週期的 Host的物件,主要封裝在nuget套件 Microsoft.Extensions.Hosting
中
當Host 啟動的時候,他會呼叫每個被註冊在容器中的IHostService
的 StartAsync()
方法
通常一個IHostService
代表他一個需要被長時間執行的Service
像是AspNet Core 的Web應用程式,本質上就是一個不斷監聽著http 請求,並對齊請求做出回應的一個HostService
IHostService.cs
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
我這邊寫了一個範例程式,每五秒會去檢查當前庫存,如果庫存量低於30會跳alertNotificationService.cs
public class NotificationService : IHostedService
{
private readonly Random _random;
public NotificationService(Random random)
{
_random = random;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
const int alertStock = 30;
while (cancellationToken.IsCancellationRequested == false)
{
var stock = GetRemainStock();
Console.WriteLine($"Current Stock: {stock}");
if (stock <= alertStock)
{
AlertLowStock(stock);
}
await Task.Delay(5000, cancellationToken);
}
}
private static void AlertLowStock(int stock)
{
Console.WriteLine($"Stock is low. Current stock is {stock}");
}
private int GetRemainStock()
{
return _random.Next(0,101);
}
public Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Stock checking monitor stopped");
return Task.CompletedTask;
}
}
這邊使用HostBuilder
物件來建立一個IHost
我們前面有提到 Host
封裝了 DI
可以看見下面是使用 HostBuilder
來註冊服務
後面會提到其他HostBuilder
的使用方式Program.cs
IHostBuilder builder = new HostBuilder();
IHost host = builder.ConfigureServices(service =>
{
service.AddSingleton(new Random());
service.AddHostedService<StockMonitorService>();
}).Build();
await host.RunAsync();
可以注意到我們上面在啟動Host的時候呼叫的是 await host.RunAsync();
而不是 await host.StartAsync()
我們來看看為什麼
IHostApplicationLifetime.cs
public interface IHostApplicationLifetime
{
/// <summary>
/// Triggered when the application host has fully started.
/// </summary>
CancellationToken ApplicationStarted { get; }
/// <summary>
/// Triggered when the application host is starting a graceful shutdown.
/// Shutdown will block until all callbacks registered on this token have completed.
/// </summary>
CancellationToken ApplicationStopping { get; }
/// <summary>
/// Triggered when the application host has completed a graceful shutdown.
/// The application will not exit until all callbacks registered on this token have completed.
/// </summary>
CancellationToken ApplicationStopped { get; }
/// <summary>
/// Requests termination of the current application.
/// </summary>
void StopApplication();
}
這個介面主要有3個CancellationToken
物件
分別對應
我們可以透過對他們來取得對應的通知
Program.cs
IHostBuilder builder = new HostBuilder();
var host = builder.ConfigureServices(service =>
{
service.AddSingleton(new Random());
service.AddHostedService<LifetimeSampleService>();
}).Build();
await host.RunAsync();
public class LifetimeSampleService : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public LifetimeSampleService(IHostApplicationLifetime hostApplicationLifetime)
{
hostApplicationLifetime.ApplicationStarted.Register(()=> Console.WriteLine("Application started"));
hostApplicationLifetime.ApplicationStopping.Register(()=> Console.WriteLine("Application stopping"));
hostApplicationLifetime.ApplicationStopped.Register(()=> Console.WriteLine("Application stopped"));
_hostApplicationLifetime = hostApplicationLifetime;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var token = new CancellationTokenSource(TimeSpan.FromSeconds(3)).Token;
token.Register(_hostApplicationLifetime.StopApplication);
await Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await Task.CompletedTask;
}
}
這邊透過CancellationToken
物件的Register
方法註冊了callback
然後在Start的時候 設定一個CancellationTokenSource
會在三秒後去觸發 StopApplication
的方法
這個方法本身是個擴充方法
(這邊簡化不全貼整個Extentsion,只貼RunAsync()
)
public static class HostingAbstractionsHostExtensions
{
public static async Task RunAsync(this IHost host, CancellationToken token = default)
{
try
{
await host.StartAsync(token).ConfigureAwait(false);
await host.WaitForShutdownAsync(token).ConfigureAwait(false);
}
finally
{
if (host is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else
{
host.Dispose();
}
}
}
}
很簡單就三個步驟
host.StartAsync
方法host.WaitForShutdownAsync
方法... 等等這個WaitForShutdownAsync
是什麼
這邊不貼程式碼,大概說一下他的實作方式為
監聽 IHostApplicationLifetime
的 ApplicationStopping
是不是觸發了
是的話叫呼叫IHost.StopAsync()
的方法
這邊以下打到一半不小心按到上一頁,因為太氣了,可能直接省略了一些內容
在.Net core的設定中,很多地方都可以看到Build Pattern
,她將複雜物件的建立抽象化IHostBuilder
就是其中一個例子
我們前面有提到Host一個封裝了應用程式資源的物件,所以裡面所要用的服務都可以透過IHostBuilder
設定
IHostBuilder.cs
public interface IHostBuilder
{
IDictionary<object, object> Properties { get; }
IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate);
IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate);
IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate);
IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) where TContainerBuilder : notnull;
IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) where TContainerBuilder : notnull;
IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate);
IHost Build();
}
ConfigureHostConfiguration
跟 ConfigureAppConfiguration
這兩個方法主要是Host跟應用程式的設定,後面提到 Configuration 會再用到。
builder.ConfigureHostConfiguration(x => x.AddJsonFile("myconfig.json"))
剩下幾個方法主要都是對DI 進行設定
我們昨天有提到,asp.net core 的DI 容器主要是透過 DefaultServiceProviderFactory
所建立的
就算你不是dotnet core應用程式也可以自訂你的DI Factory
builder.UseServiceProviderFactory(x => new DefaultServiceProviderFactory())
其他像log 之類的Config方式則放在另一個Extension class HostingHostBuilderExtensions.cs 原始碼就不特別貼了,在後面講log 的時候會再提到
可以注意到在IHostBuilder
中的 config 方法很多都有類別HostBuilderContext
的存在
HostBuilderContext.cs
public class HostBuilderContext
{
public HostBuilderContext(IDictionary<object, object> properties)
{
ThrowHelper.ThrowIfNull(properties);
Properties = properties;
}
public IHostEnvironment HostingEnvironment { get; set; } = null!;
public IConfiguration Configuration { get; set; } = null!;
public IDictionary<object, object> Properties { get; }
}
在ConfigureHostConfiguration
跟 ConfigureAppConfiguration
這兩個方法中產生Configuration 最終彙整成同一份IConfiguration
Properties
則是在做設定時,有前後相依同個變數,可以從其中去取得
我們常常會需要根據不同的執行環境(ex. 本機,生產)做不同的事情
這些資訊一樣會封裝在HostBuilderContext.HostingEnvironment
中
可以透過擴充方法IHostEnvironment.IsEnvironment(string environment)
來判斷當前環境是否為你想要的環境
IHostEnvironment.cs
public interface IHostEnvironment
{
string EnvironmentName { get; set; }
string ApplicationName { get; set; }
string ContentRootPath { get; set; }
IFileProvider ContentRootFileProvider { get; set; }
}
這邊做個Sample
IHostBuilder builder = new HostBuilder();
// HostBuilder 如果沒特別設定,預設環境為 Production
args = args.Append("environment=Development").ToArray();
var host =
// 設定Config(包含環境)
.ConfigureHostConfiguration(x => x.AddCommandLine(args))
.ConfigureServices(x => x.AddHostedService<EnvSampleService>())
.Build();
await host.RunAsync();
public class EnvSampleService : IHostedService
{
private readonly IHostEnvironment _env;
public EnvSampleService(IHostEnvironment env)
{
_env = env;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine($"Environment: {_env.EnvironmentName}");
Console.WriteLine($"isDevelopment: {_env.IsEnvironment("Development")}");
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await Task.CompletedTask;
}
}
我們透過arg 設定當前環境為Development
這邊節錄了HostBuilder
的Build 方法
HostBuilder.cs
public IHost Build()
{
if (_hostBuilt)
{
throw new InvalidOperationException(SR.BuildCalled);
}
_hostBuilt = true;
// REVIEW: If we want to raise more events outside of these calls then we will need to
// stash this in a field.
using var diagnosticListener = new DiagnosticListener("Microsoft.Extensions.Hosting");
const string hostBuildingEventName = "HostBuilding";
const string hostBuiltEventName = "HostBuilt";
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuildingEventName))
{
Write(diagnosticListener, hostBuildingEventName, this);
}
// 我們把重點放在這段以下
BuildHostConfiguration();
CreateHostingEnvironment();
CreateHostBuilderContext();
BuildAppConfiguration();
CreateServiceProvider();
// 可以看見Host 物件是由DI容器提供,並非由Build方法建立
var host = _appServices.GetRequiredService<IHost>();
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuiltEventName))
{
Write(diagnosticListener, hostBuiltEventName, host);
}
return host;
}
建立Host 大概有幾個步驟
IConfiguration
)IHostEnvironment
HostBuilderContext
其實就是把剛剛那兩個塞進去IConfiguration
再assign回HostBuilderContext.IConfiguration
CreateServiceProvider()
)這邊針對2跟5做補充
CreateHostingEnvironment
我在上面的例子有提到,CreateBuilder 如無特別設,當前環境預設會是Production
,原因就在這個方法中
private void CreateServiceProvider()
{
var services = new ServiceCollection();
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
#pragma warning restore CS0618 // Type or member is obsolete
// 這邊註冊了IHostEnvironment,所以可以在服務中注入
services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
services.AddSingleton(_hostBuilderContext);
// register configuration as factory to make it dispose with the service provider
services.AddSingleton(_ => _appConfiguration);
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
AddLifetime(services);
// Host 在這裡建立
services.AddSingleton<IHost>(_ =>
{
return new Internal.Host(_appServices,
_hostingEnvironment,
_defaultProvider,
_appServices.GetRequiredService<IHostApplicationLifetime>(),
_appServices.GetRequiredService<ILogger<Internal.Host>>(),
_appServices.GetRequiredService<IHostLifetime>(),
_appServices.GetRequiredService<IOptions<HostOptions>>());
});
services.AddOptions().Configure<HostOptions>(options => { options.Initialize(_hostConfiguration); });
services.AddLogging();
// 這邊會把我們在外面註冊的Service加到IServiceCollection 中
foreach (Action<HostBuilderContext, IServiceCollection> configureServicesAction in _configureServicesActions)
{
configureServicesAction(_hostBuilderContext, services);
}
object containerBuilder = _serviceProviderFactory.CreateBuilder(services);
foreach (IConfigureContainerAdapter containerAction in _configureContainerActions)
{
containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder);
}
_appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder);
if (_appServices == null)
{
throw new InvalidOperationException(SR.NullIServiceProvider);
}
// resolve configuration explicitly once to mark it as resolved within the
// service provider, ensuring it will be properly disposed with the provider
_ = _appServices.GetService<IConfiguration>();
}