乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 泛型主機(Host),封裝應用資源和生存期功能

什麼是泛型主機

泛型主機(Host),又名通用主機,是封裝應用資源和生存期功能的對象。

一個對象中包含所有應用的相互依賴資源的主要原因是生存期管理:控制應用啓動正常關閉

其中包括:

  • 依賴關係注入(DI)
  • 日誌記錄(Logging)
  • 應用配置(Configuration)
  • 應用關閉
  • 主機服務實現(IHostedService)

啓動

  • 當泛型主機啓動時,它將對在託管服務的服務容器集合中註冊的IHostedService的每個實現調用IHostedService.StartAsync
  • 在輔助角色服務應用中,包含BackgroundService實例的所有IHostedService實現都調用其BackgroundService.ExecuteAsync方法。

Nuget包

https://www.nuget.org/packages/Microsoft.Extensions.Hosting

dotnet add package Microsoft.Extensions.Hosting

image

基礎使用(ConsoleApp)

static void Main(string[] args)
{
    IHost host = Host
    // 創建生成器對象
    .CreateDefaultBuilder(args)
    // 配置生成器對象
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<AppHostService>();
    })
    // 創建IHost實例
    .Build();
    // 對主機對象調用運行方法
    host.Run();

    Console.WriteLine("Hello, World!");
}

基礎使用(WpfApp)

定義應用主機服務AppHostService,它需要繼承並實現IHostedService接口

/// <summary>
/// 應用主機服務
/// </summary>
public class AppHostService : IHostedService
{
    /// <summary>
    /// 容器服務提供者
    /// </summary>
    private readonly IServiceProvider _serviceProvider;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="serviceProvider"></param>
    public AppHostService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    /// <summary>
    /// 啓動主機
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        var mainWindow = _serviceProvider.GetService<MainWindow>();
        mainWindow.Show();
        return Task.CompletedTask;
    }

    /// <summary>
    /// 停止主機
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

App.xaml.cs的使用示例

public partial class App : Application
{
    /// <summary>
    /// 構建主機對象
    /// </summary>
    private static readonly IHost _host = Host
        // 創建生成器對象
        .CreateDefaultBuilder()
        // 配置生成器對象
        .ConfigureServices((hostContext, services) =>
        {
            // 添加應用主機服務
            services.AddHostedService<AppHostService>();
            // 添加窗體
            services.AddSingleton<MainWindow>();
        })
        // 創建IHost實例
        .Build();

    /// <summary>
    /// 獲取應用服務
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T GetService<T>() where T : class
    {
        return _host.Services.GetService<T>();
    }

    /// <summary>
    /// 應用啓動事件
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private async void OnStartup(object sender, StartupEventArgs e)
    {
        await _host.RunAsync();
    }

    /// <summary>
    /// 應用退出事件
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private async void OnExit(object sender, ExitEventArgs e)
    {
        await _host.StopAsync();
        _host.Dispose();
    }
}

基礎使用(WebApi)

.Net Core 3.1

WebApi項目的入口Program.Main,它調用靜態方法CreateHostBuilder來創建並配置主機生成器對象。

public class Program
{
    public static void Main(string[] args)
    {
        // 創建並配置主機生成器對象、創建實例、運行實例
        CreateHostBuilder(args).Build().Run();
    }

    /// <summary>
    /// 創建並配置生成器對象
    /// </summary>
    /// <param name="args"></param>
    /// <returns></returns>
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        // 創建生成器對象
        Host.CreateDefaultBuilder(args)
            // 配置Web生成器對象
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

在配置Web生成器對象中使用了Startup這個類,這時候已經可以從容器獲取IConfiguration對象了。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

.Net 6/.Net 7

在.Net 6/.Net 7中已經換成了這樣。

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.

        builder.Services.AddControllers();
        // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen();

        var app = builder.Build();

        // Configure the HTTP request pipeline.
        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }

        app.UseHttpsRedirection();

        app.UseAuthorization();


        app.MapControllers();

        app.Run();
    }
}

而這裏用到的WebApplication是繼承自接口IHost的實現。

/// <summary>
/// The web application used to configure the HTTP pipeline, and routes.
/// </summary>
public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
{
    internal const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";

    private readonly IHost _host;
    private readonly List<EndpointDataSource> _dataSources = new();

    internal WebApplication(IHost host)
    {
        _host = host;
        ApplicationBuilder = new ApplicationBuilder(host.Services, ServerFeatures);
        Logger = host.Services.GetRequiredService<ILoggerFactory>().CreateLogger(Environment.ApplicationName);

        Properties[GlobalEndpointRouteBuilderKey] = this;
    }

    /// <summary>
    /// The application's configured services.
    /// </summary>
    public IServiceProvider Services => _host.Services;

    /// <summary>
    /// The application's configured <see cref="IConfiguration"/>.
    /// </summary>
    public IConfiguration Configuration => _host.Services.GetRequiredService<IConfiguration>();

    /// <summary>
    /// The application's configured <see cref="IWebHostEnvironment"/>.
    /// </summary>
    public IWebHostEnvironment Environment => _host.Services.GetRequiredService<IWebHostEnvironment>();

    /// <summary>
    /// Allows consumers to be notified of application lifetime events.
    /// </summary>
    public IHostApplicationLifetime Lifetime => _host.Services.GetRequiredService<IHostApplicationLifetime>();

    /// <summary>
    /// The default logger for the application.
    /// </summary>
    public ILogger Logger { get; }

    /// <summary>
    /// The list of URLs that the HTTP server is bound to.
    /// </summary>
    public ICollection<string> Urls => ServerFeatures.Get<IServerAddressesFeature>()?.Addresses ??
        throw new InvalidOperationException($"{nameof(IServerAddressesFeature)} could not be found.");

    IServiceProvider IApplicationBuilder.ApplicationServices
    {
        get => ApplicationBuilder.ApplicationServices;
        set => ApplicationBuilder.ApplicationServices = value;
    }

    internal IFeatureCollection ServerFeatures => _host.Services.GetRequiredService<IServer>().Features;
    IFeatureCollection IApplicationBuilder.ServerFeatures => ServerFeatures;

    internal IDictionary<string, object?> Properties => ApplicationBuilder.Properties;
    IDictionary<string, object?> IApplicationBuilder.Properties => Properties;

    internal ICollection<EndpointDataSource> DataSources => _dataSources;
    ICollection<EndpointDataSource> IEndpointRouteBuilder.DataSources => DataSources;

    internal ApplicationBuilder ApplicationBuilder { get; }

    IServiceProvider IEndpointRouteBuilder.ServiceProvider => Services;

    /// <summary>
    /// Initializes a new instance of the <see cref="WebApplication"/> class with preconfigured defaults.
    /// </summary>
    /// <param name="args">Command line arguments</param>
    /// <returns>The <see cref="WebApplication"/>.</returns>
    public static WebApplication Create(string[]? args = null) =>
        new WebApplicationBuilder(new() { Args = args }).Build();

    /// <summary>
    /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
    /// </summary>
    /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
    public static WebApplicationBuilder CreateBuilder() =>
        new(new());

    /// <summary>
    /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
    /// </summary>
    /// <param name="args">Command line arguments</param>
    /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
    public static WebApplicationBuilder CreateBuilder(string[] args) =>
        new(new() { Args = args });

    /// <summary>
    /// Initializes a new instance of the <see cref="WebApplicationBuilder"/> class with preconfigured defaults.
    /// </summary>
    /// <param name="options">The <see cref="WebApplicationOptions"/> to configure the <see cref="WebApplicationBuilder"/>.</param>
    /// <returns>The <see cref="WebApplicationBuilder"/>.</returns>
    public static WebApplicationBuilder CreateBuilder(WebApplicationOptions options) =>
        new(options);

    /// <summary>
    /// Start the application.
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns>
    /// A <see cref="Task"/> that represents the startup of the <see cref="WebApplication"/>.
    /// Successful completion indicates the HTTP server is ready to accept new requests.
    /// </returns>
    public Task StartAsync(CancellationToken cancellationToken = default) =>
        _host.StartAsync(cancellationToken);

    /// <summary>
    /// Shuts down the application.
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns>
    /// A <see cref="Task"/> that represents the shutdown of the <see cref="WebApplication"/>.
    /// Successful completion indicates that all the HTTP server has stopped.
    /// </returns>
    public Task StopAsync(CancellationToken cancellationToken = default) =>
        _host.StopAsync(cancellationToken);

    /// <summary>
    /// Runs an application and returns a Task that only completes when the token is triggered or shutdown is triggered.
    /// </summary>
    /// <param name="url">The URL to listen to if the server hasn't been configured directly.</param>
    /// <returns>
    /// A <see cref="Task"/> that represents the entire runtime of the <see cref="WebApplication"/> from startup to shutdown.
    /// </returns>
    public Task RunAsync(string? url = null)
    {
        Listen(url);
        return HostingAbstractionsHostExtensions.RunAsync(this);
    }

    /// <summary>
    /// Runs an application and block the calling thread until host shutdown.
    /// </summary>
    /// <param name="url">The URL to listen to if the server hasn't been configured directly.</param>
    public void Run(string? url = null)
    {
        Listen(url);
        HostingAbstractionsHostExtensions.Run(this);
    }

    /// <summary>
    /// Disposes the application.
    /// </summary>
    void IDisposable.Dispose() => _host.Dispose();

    /// <summary>
    /// Disposes the application.
    /// </summary>
    public ValueTask DisposeAsync() => ((IAsyncDisposable)_host).DisposeAsync();

    internal RequestDelegate BuildRequestDelegate() => ApplicationBuilder.Build();
    RequestDelegate IApplicationBuilder.Build() => BuildRequestDelegate();

    // REVIEW: Should this be wrapping another type?
    IApplicationBuilder IApplicationBuilder.New()
    {
        var newBuilder = ApplicationBuilder.New();
        // Remove the route builder so branched pipelines have their own routing world
        newBuilder.Properties.Remove(GlobalEndpointRouteBuilderKey);
        return newBuilder;
    }

    IApplicationBuilder IApplicationBuilder.Use(Func<RequestDelegate, RequestDelegate> middleware)
    {
        ApplicationBuilder.Use(middleware);
        return this;
    }

    IApplicationBuilder IEndpointRouteBuilder.CreateApplicationBuilder() => ((IApplicationBuilder)this).New();

    private void Listen(string? url)
    {
        if (url is null)
        {
            return;
        }

        var addresses = ServerFeatures.Get<IServerAddressesFeature>()?.Addresses;
        if (addresses is null)
        {
            throw new InvalidOperationException($"Changing the URL is not supported because no valid {nameof(IServerAddressesFeature)} was found.");
        }
        if (addresses.IsReadOnly)
        {
            throw new InvalidOperationException($"Changing the URL is not supported because {nameof(IServerAddressesFeature.Addresses)} {nameof(ICollection<string>.IsReadOnly)}.");
        }

        addresses.Clear();
        addresses.Add(url);
    }
}

默認設置

Host名下存在靜態方法CreateDefaultBuilder

  • 將內容根目錄設置爲當前程序運行目錄(CreateHostingEnvironment)
  • 添加環境變量和命令行解析(ApplyDefaultHostConfiguration)
  • 應用默認應用配置(ApplyDefaultAppConfiguration)
  • 添加默認服務(AddDefaultServices),含日誌服務
  • 開發模式啓用範圍驗證和依賴關係驗證(CreateDefaultServiceProviderOptions)

它定義爲

public static class Host
{
    [RequiresDynamicCode(RequiresDynamicCodeMessage)]
    public static IHostBuilder CreateDefaultBuilder() =>
        CreateDefaultBuilder(args: null);

    [RequiresDynamicCode(RequiresDynamicCodeMessage)]
    public static IHostBuilder CreateDefaultBuilder(string[]? args)
    {
        HostBuilder builder = new();
        return builder.ConfigureDefaults(args);
    }
}

它的核心是調用另外一個靜態擴展方法HostingHostBuilderExtensions.ConfigureDefaults

public static class HostingHostBuilderExtensions
{
    [RequiresDynamicCode(Host.RequiresDynamicCodeMessage)]
    public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[]? args)
    {
        return builder.ConfigureHostConfiguration(config => ApplyDefaultHostConfiguration(config, args))
                        .ConfigureAppConfiguration((hostingContext, config) => ApplyDefaultAppConfiguration(hostingContext, config, args))
                        .ConfigureServices(AddDefaultServices)
                        .UseServiceProviderFactory(context => new DefaultServiceProviderFactory(CreateDefaultServiceProviderOptions(context)));
    }
}

將內容根目錄設置爲當前程序運行目錄(CreateHostingEnvironment)

/// <summary>
/// Constants for HostBuilder configuration keys.
/// </summary>
public static class HostDefaults
{
    /// <summary>
    /// The configuration key used to set <see cref="IHostEnvironment.ApplicationName"/>.
    /// </summary>
    public static readonly string ApplicationKey = "applicationName";

    /// <summary>
    /// The configuration key used to set <see cref="IHostEnvironment.EnvironmentName"/>.
    /// </summary>
    public static readonly string EnvironmentKey = "environment";

    /// <summary>
    /// The configuration key used to set <see cref="IHostEnvironment.ContentRootPath"/>
    /// and <see cref="IHostEnvironment.ContentRootFileProvider"/>.
    /// </summary>
    public static readonly string ContentRootKey = "contentRoot";
}

internal static (HostingEnvironment, PhysicalFileProvider) CreateHostingEnvironment(IConfiguration hostConfiguration)
{
    var hostingEnvironment = new HostingEnvironment()
    {
        EnvironmentName = hostConfiguration[HostDefaults.EnvironmentKey] ?? Environments.Production,
        ContentRootPath = ResolveContentRootPath(hostConfiguration[HostDefaults.ContentRootKey], AppContext.BaseDirectory),
    };

    string? applicationName = hostConfiguration[HostDefaults.ApplicationKey];
    if (string.IsNullOrEmpty(applicationName))
    {
        // Note GetEntryAssembly returns null for the net4x console test runner.
        applicationName = Assembly.GetEntryAssembly()?.GetName().Name;
    }

    if (applicationName is not null)
    {
        hostingEnvironment.ApplicationName = applicationName;
    }

    var physicalFileProvider = new PhysicalFileProvider(hostingEnvironment.ContentRootPath);
    hostingEnvironment.ContentRootFileProvider = physicalFileProvider;

    return (hostingEnvironment, physicalFileProvider);
}

internal static 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);
}

添加環境變量和命令行解析(ApplyDefaultHostConfiguration)

internal static void ApplyDefaultHostConfiguration(IConfigurationBuilder hostConfigBuilder, string[]? args)
{
    // If we're running anywhere other than C:\Windows\system32, we default to using the CWD for the ContentRoot.
    // However, since many things like Windows services and MSIX installers have C:\Windows\system32 as there CWD which is not likely
    // to really be the home for things like appsettings.json, we skip changing the ContentRoot in that case. The non-"default" initial
    // value for ContentRoot is AppContext.BaseDirectory (e.g. the executable path) which probably makes more sense than the system32.

    // In my testing, both Environment.CurrentDirectory and Environment.GetFolderPath(Environment.SpecialFolder.System) return the path without
    // any trailing directory separator characters. I'm not even sure the casing can ever be different from these APIs, but I think it makes sense to
    // ignore case for Windows path comparisons given the file system is usually (always?) going to be case insensitive for the system path.
    string cwd = Environment.CurrentDirectory;
    if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !string.Equals(cwd, Environment.GetFolderPath(Environment.SpecialFolder.System), StringComparison.OrdinalIgnoreCase))
    {
        hostConfigBuilder.AddInMemoryCollection(new[]
        {
            new KeyValuePair<string, string?>(HostDefaults.ContentRootKey, cwd),
        });
    }

    hostConfigBuilder.AddEnvironmentVariables(prefix: "DOTNET_");
    if (args is { Length: > 0 })
    {
        hostConfigBuilder.AddCommandLine(args);
    }
}

應用默認應用配置(ApplyDefaultAppConfiguration)

應用默認應用配置(ApplyDefaultAppConfiguration),這裏我們看到實際上它將appsettings.jsonappsettings.{env.EnvironmentName}.json兩個配置都加載進來了;而且在Development環境下,添加密鑰管理器(AddUserSecrets);添加環境變量(AddEnvironmentVariables);添加命令行參數(AddCommandLine)的支持。

internal static void ApplyDefaultAppConfiguration(HostBuilderContext hostingContext, IConfigurationBuilder appConfigBuilder, string[]? args)
{
    IHostEnvironment env = hostingContext.HostingEnvironment;
    bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);

    appConfigBuilder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);

    if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
    {
        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
        if (appAssembly is not null)
        {
            appConfigBuilder.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange);
        }
    }

    appConfigBuilder.AddEnvironmentVariables();

    if (args is { Length: > 0 })
    {
        appConfigBuilder.AddCommandLine(args);
    }

    [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Calling IConfiguration.GetValue is safe when the T is bool.")]
    static bool GetReloadConfigOnChangeValue(HostBuilderContext hostingContext) => hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
}

添加默認服務(AddDefaultServices),含日誌服務

添加默認服務(AddDefaultServices),這裏我們看到添加了日誌服務,且日誌服務的配置來自Logging節點。這裏添加了控制檯(AddConsole)、調試(AddDebug)、事件源(AddEventSourceLogger)、事件日誌(AddEventLog)日誌提供程序。

internal static void AddDefaultServices(HostBuilderContext hostingContext, IServiceCollection services)
{
    services.AddLogging(logging =>
    {
        bool isWindows =
#if NETCOREAPP
            OperatingSystem.IsWindows();
#else
            RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif

        // IMPORTANT: This needs to be added *before* configuration is loaded, this lets
        // the defaults be overridden by the configuration.
        if (isWindows)
        {
            // Default the EventLogLoggerProvider to warning or above
            logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
        }

        logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
#if NETCOREAPP
        if (!OperatingSystem.IsBrowser())
#endif
        {
            logging.AddConsole();
        }
        logging.AddDebug();
        logging.AddEventSourceLogger();

        if (isWindows)
        {
            // Add the EventLogLoggerProvider on windows machines
            logging.AddEventLog();
        }

        logging.Configure(options =>
        {
            options.ActivityTrackingOptions =
                ActivityTrackingOptions.SpanId |
                ActivityTrackingOptions.TraceId |
                ActivityTrackingOptions.ParentId;
        });
    });
}

開發模式啓用範圍驗證和依賴關係驗證(CreateDefaultServiceProviderOptions)

internal static ServiceProviderOptions CreateDefaultServiceProviderOptions(HostBuilderContext context)
{
    bool isDevelopment = context.HostingEnvironment.IsDevelopment();
    return new ServiceProviderOptions
    {
        ValidateScopes = isDevelopment,
        ValidateOnBuild = isDevelopment,
    };
}

內置服務

主機默認註冊了以下三個服務

  • 主機應用生命週期(IHostApplicationLifetime)
  • 主機生命週期(IHostLifetime)
  • 主機環境(IHostEnvironment)

主機應用生命週期(IHostApplicationLifetime)

允許通知使用者應用程序生存期事件

  • 在應用程序主機完全啓動時觸發(ApplicationStarted)
  • 在應用程序主機執行正常關閉時觸發(ApplicationStopped)
  • 當應用程序主機執行正常關閉時觸發(ApplicationStopping)。在此事件完成之前,關閉將被阻止。

其定義是

/// <summary>
/// Allows consumers to be notified of application lifetime events. This interface is not intended to be user-replaceable.
/// </summary>
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();
}

使用示例

/// <summary>
/// 應用主機服務
/// </summary>
public class AppHostService : IHostedService
{
    /// <summary>
    /// 容器服務提供者
    /// </summary>
    private readonly IServiceProvider _serviceProvider;

    /// <summary>
    /// 日誌服務
    /// </summary>
    private readonly ILogger _logger;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="serviceProvider"></param>
    public AppHostService(ILogger<AppHostService> logger, IServiceProvider serviceProvider, IHostApplicationLifetime hostApplicationLifetime)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;

        // 註冊應用已啓動事件處理方法
        hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
        // 註冊應用正在停止事件處理方法
        hostApplicationLifetime.ApplicationStopping.Register(OnStopping);
        // 註冊應用已停止事件處理方法
        hostApplicationLifetime.ApplicationStopped.Register(OnStopped);
        // 可選調用停止應用方法
        //hostApplicationLifetime.StopApplication();
    }

    /// <summary>
    /// 應用已啓動
    /// </summary>
    private void OnStarted()
    {
        _logger.LogInformation("App Started");
    }

    /// <summary>
    /// 應用正在停止
    /// </summary>
    private void OnStopping()
    {
        _logger.LogInformation("App Stopping");
    }

    /// <summary>
    /// 應用已停止
    /// </summary>
    private void OnStopped()
    {
        _logger.LogInformation("App Stopped");
    }

    /// <summary>
    /// 啓動主機
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Host Start");
        var mainWindow = _serviceProvider.GetService<MainWindow>();
        mainWindow.Show();
        return Task.CompletedTask;
    }

    /// <summary>
    /// 停止主機
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Host Stop");
        return Task.CompletedTask;
    }
}

輸出

demoForHostWpf60.AppHostService: Information: Host Start
demoForHostWpf60.AppHostService: Information: App Started
Microsoft.Hosting.Lifetime: Information: Application started. Press Ctrl+C to shut down.
Microsoft.Hosting.Lifetime: Information: Hosting environment: Production
Microsoft.Hosting.Lifetime: Information: Content root path: demoForHostApp60\bin\Debug\net6.0-windows
demoForHostWpf60.AppHostService: Information: App Stopping
Microsoft.Hosting.Lifetime: Information: Application is shutting down...
demoForHostWpf60.AppHostService: Information: Host Stop
demoForHostWpf60.AppHostService: Information: App Stopped

主機生命週期(IHostLifetime)

主機生命週期(IHostLifetime)用於控制主機何時啓動、何時停止。

  • 指示主機正在停止並且可以關閉(StopAsync(CancellationToken))
  • StartAsync(CancellationToken)開始時調用,在繼續之前,會一直等待該操作完成。它可用於延遲啓動,直到外部事件發出信號(WaitForStartAsync(CancellationToken))。

其定義是

public interface IHostLifetime
{
    /// <summary>
    /// Called at the start of <see cref="IHost.StartAsync(CancellationToken)"/> which will wait until it's complete before
    /// continuing. This can be used to delay startup until signaled by an external event.
    /// </summary>
    /// <param name="cancellationToken">Used to abort program start.</param>
    /// <returns>A <see cref="Task"/>.</returns>
    Task WaitForStartAsync(CancellationToken cancellationToken);

    /// <summary>
    /// Called from <see cref="IHost.StopAsync(CancellationToken)"/> to indicate that the host is stopping and it's time to shut down.
    /// </summary>
    /// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
    /// <returns>A <see cref="Task"/>.</returns>
    Task StopAsync(CancellationToken cancellationToken);
}

它將使用註冊的最後一個實現,但是默認實現是Microsoft.Extensions.Hosting.Internal.ConsoleLifetime,我們可以看下ConsoleLifetime的定義。

/// <summary>
/// Listens for Ctrl+C or SIGTERM and initiates shutdown.
/// </summary>
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("tvos")]
public partial class ConsoleLifetime : IHostLifetime, IDisposable
{
    private CancellationTokenRegistration _applicationStartedRegistration;
    private CancellationTokenRegistration _applicationStoppingRegistration;

    public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, IOptions<HostOptions> hostOptions)
        : this(options, environment, applicationLifetime, hostOptions, NullLoggerFactory.Instance) { }

    public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, IOptions<HostOptions> hostOptions, ILoggerFactory loggerFactory)
    {
        ThrowHelper.ThrowIfNull(options?.Value, nameof(options));
        ThrowHelper.ThrowIfNull(applicationLifetime);
        ThrowHelper.ThrowIfNull(environment);
        ThrowHelper.ThrowIfNull(hostOptions?.Value, nameof(hostOptions));

        Options = options.Value;
        Environment = environment;
        ApplicationLifetime = applicationLifetime;
        HostOptions = hostOptions.Value;
        Logger = loggerFactory.CreateLogger("Microsoft.Hosting.Lifetime");
    }

    private ConsoleLifetimeOptions Options { get; }

    private IHostEnvironment Environment { get; }

    private IHostApplicationLifetime ApplicationLifetime { get; }

    private HostOptions HostOptions { get; }

    private ILogger Logger { get; }

    public Task WaitForStartAsync(CancellationToken cancellationToken)
    {
        if (!Options.SuppressStatusMessages)
        {
            _applicationStartedRegistration = ApplicationLifetime.ApplicationStarted.Register(state =>
            {
                ((ConsoleLifetime)state!).OnApplicationStarted();
            },
            this);
            _applicationStoppingRegistration = ApplicationLifetime.ApplicationStopping.Register(state =>
            {
                ((ConsoleLifetime)state!).OnApplicationStopping();
            },
            this);
        }

        RegisterShutdownHandlers();

        // Console applications start immediately.
        return Task.CompletedTask;
    }

    private partial void RegisterShutdownHandlers();

    private void OnApplicationStarted()
    {
        Logger.LogInformation("Application started. Press Ctrl+C to shut down.");
        Logger.LogInformation("Hosting environment: {EnvName}", Environment.EnvironmentName);
        Logger.LogInformation("Content root path: {ContentRoot}", Environment.ContentRootPath);
    }

    private void OnApplicationStopping()
    {
        Logger.LogInformation("Application is shutting down...");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // There's nothing to do here
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        UnregisterShutdownHandlers();

        _applicationStartedRegistration.Dispose();
        _applicationStoppingRegistration.Dispose();
    }

    private partial void UnregisterShutdownHandlers();
}

這裏我們看到,其實前面我們看到的一些默認輸出,其實就是它做的。

它這裏也是註冊並監聽了應用已啓動事件,在OnApplicationStarted中寫了一些日誌

Logger.LogInformation("Application started. Press Ctrl+C to shut down.");
Logger.LogInformation("Hosting environment: {EnvName}", Environment.EnvironmentName);
Logger.LogInformation("Content root path: {ContentRoot}", Environment.ContentRootPath);

它註冊並監聽了應用即將停止事件,在OnApplicationStopping中寫了一些日誌

Logger.LogInformation("Application is shutting down...");

而且它的日誌標記叫Microsoft.Hosting.Lifetime

public partial class ConsoleLifetime : IHostLifetime, IDisposable
{

    public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, IOptions<HostOptions> hostOptions, ILoggerFactory loggerFactory)
    {
        ThrowHelper.ThrowIfNull(options?.Value, nameof(options));
        ThrowHelper.ThrowIfNull(applicationLifetime);
        ThrowHelper.ThrowIfNull(environment);
        ThrowHelper.ThrowIfNull(hostOptions?.Value, nameof(hostOptions));

        Options = options.Value;
        Environment = environment;
        ApplicationLifetime = applicationLifetime;
        HostOptions = hostOptions.Value;
        Logger = loggerFactory.CreateLogger("Microsoft.Hosting.Lifetime");
    }
}

所以最終輸出纔是

Microsoft.Hosting.Lifetime: Information: Application started. Press Ctrl+C to shut down.
Microsoft.Hosting.Lifetime: Information: Hosting environment: Production
Microsoft.Hosting.Lifetime: Information: Content root path: demoForHostApp60\bin\Debug\net6.0-windows
Microsoft.Hosting.Lifetime: Information: Application is shutting down...

主機環境(IHostEnvironment)

提供有關其中正在運行應用程序的宿主環境的信息

主機環境(IHostEnvironment)可用於獲取環境設置信息。

  • 獲取或設置應用程序的名稱(ApplicationName), 主機自動將此屬性設置爲包含應用程序入口點的程序集。
  • 獲取或設置環境的名稱(EnvironmentName), 主機自動將此屬性設置爲配置中指定的"環境"鍵的值。
  • 獲取或設置包含應用程序內容文件的目錄的絕對路徑(ContentRootPath)。
  • 獲取或設置指向ContentRootPath的IFileProvider(ContentRootFileProvider)。

其定義如下

/// <summary>
/// Provides information about the hosting environment an application is running in.
/// </summary>
public interface IHostEnvironment
{
    /// <summary>
    /// Gets or sets the name of the environment. The host automatically sets this property to the value 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; }
}

我們來看看它的默認實現Microsoft.Extensions.Hosting.Internal.HostingEnvironment

public class HostingEnvironment : IHostingEnvironment, IHostEnvironment
{
    public string EnvironmentName { get; set; } = string.Empty;

    public string ApplicationName { get; set; } = string.Empty;

    public string ContentRootPath { get; set; } = string.Empty;

    public IFileProvider ContentRootFileProvider { get; set; } = null!;
}

使用示例

/// <summary>
/// 構造函數
/// </summary>
public AppHostService(ILogger<AppHostService> logger, IHostEnvironment hostEnvironment)
{
    _logger = logger;

    // 應用當前設置環境
    _logger.LogInformation("App EnvironmentName:{0}", hostEnvironment.EnvironmentName);
    // 應用當前設置名稱
    _logger.LogInformation("App ApplicationName:{0}", hostEnvironment.ApplicationName);
    // 應用當前文件根目錄
    _logger.LogInformation("App ContentRootPath:{0}", hostEnvironment.ContentRootPath);
    // 應用當前文件提供方
    _logger.LogInformation("App ContentRootFileProvider:{0}", hostEnvironment.ContentRootFileProvider);
}

輸出結果

demoForHostWpf60.AppHostService: Information: App EnvironmentName:Production
demoForHostWpf60.AppHostService: Information: App ApplicationName:demoForHostWpf60
demoForHostWpf60.AppHostService: Information: App ContentRootPath:\demoForHostApp60\bin\Debug\net6.0-windows
demoForHostWpf60.AppHostService: Information: App ContentRootFileProvider:Microsoft.Extensions.FileProviders.PhysicalFileProvider

主機配置

在創建默認生成器對象之後將得到一個IHostBuilder對象,基於它存在擴展方法ConfigureHostConfiguration,可追加主機配置。

static void Main(string[] args)
{
    IHost host = Host
    // 創建生成器對象
    .CreateDefaultBuilder(args)
    // 追加主機配置
    .ConfigureHostConfiguration(configHost =>
    {
        configHost.SetBasePath(Directory.GetCurrentDirectory());
        configHost.AddJsonFile("hostsettings.json", optional: true);
        configHost.AddEnvironmentVariables(prefix: "PREFIX_");
        configHost.AddCommandLine(args);
    })
    // 配置生成器對象
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<AppHostService>();
    })
    // 創建IHost實例
    .Build();
    // 對主機對象調用運行方法
    host.Run();

    Console.WriteLine("Hello, World!");
}

應用配置

在創建默認生成器對象之後將得到一個IHostBuilder對象,基於它存在擴展方法ConfigureAppConfiguration,可追加應用配置。

image

static void Main(string[] args)
{
    IHost host = Host
    // 創建生成器對象
    .CreateDefaultBuilder(args)
    // 追加應用配置
    .ConfigureAppConfiguration(configApp =>
    {
        configApp.SetBasePath(Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location));
    })
    // 配置生成器對象
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<AppHostService>();
    })
    // 創建IHost實例
    .Build();
    // 對主機對象調用運行方法
    host.Run();

    Console.WriteLine("Hello, World!");
}

主機關閉

image

  1. 控制從ConsoleLifetime流向ApplicationLifetime,引發ApplicationStopping事件。這會指示WaitForShutdownAsync解除阻止Main執行代碼。同時,由於POSIX信號已經過處理,POSIX信號處理程序返回Cancel=true。
  2. Main執行代碼將再次開始執行,並指示主機StopAsync(),進而停止所有託管服務,並引發任何其他已停止的事件。
  3. 最後,WaitForShutdown退出,使任何應用程序清理代碼可執行且Main方法可正常退出。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章