Microsoft.Extensions.Hosting使用指南:构建现代 .NET 应用的通用宿主架构

在传统.NET应用中,程序入口通常承担大量职责:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static void Main(string[] args) {
  var plc = new PlcService();
  plc.Connect();

  var camera = new CameraService();
  camera.Open();

  while (true) {
    plc.Read();
  }
}

随着应用规模增长,Main方法会逐渐演变成创建各种对象、管理对象生命周期、初始化资源、启动后台线程、处理配置、初始化日志等等,最终形成一个巨大的入口文件。对于工业软件、桌面软件、后台服务等长期运行程序,这种方式存在明显问题:

  • 模块之间高度耦合
  • 生命周期管理困难
  • 难以测试
  • 服务无法复用
  • 资源释放容易遗漏

因此,现代.NET引入了通用宿主(Generic Host)。它的核心思想:

将应用程序中的各种模块注册到Host中,由Host统一负责创建、启动、运行和关闭。

整体架构:

图1 Host整体架构
图1 Host整体架构

什么是Host?

Host可以理解为管理应用生命周期的运行环境。类似Web Host管理Web Application,Docker Host管理Container。Host本身不负责业务逻辑。

Host负责:

  • 创建服务
  • 管理依赖
  • 加载配置
  • 初始化日志
  • 启动后台服务
  • 优雅关闭

业务代码负责:

  • PLC如何通迅
  • 相机如何采集
  • 算法如何计算

创建Host

安装:

1
dotnet add package Microsoft.Extensions.Hosting

创建:

1
2
3
4
5
6
7
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

var host = builder.Build();

await host.RunAsync();

这是一个最基础的Host应用,执行流程:

图表 代码
flowchart TD
    A[CreateApplicationBuilder] --> B[创建Host Builder]
    B --> C[注册服务]
    C --> D[Build]
    D --> E[RunAsync]
    E --> F[应用执行]
flowchart TD
    A[CreateApplicationBuilder] --> B[创建Host Builder]
    B --> C[注册服务]
    C --> D[Build]
    D --> E[RunAsync]
    E --> F[应用执行]
flowchart TD
    A[CreateApplicationBuilder] --> B[创建Host Builder]
    B --> C[注册服务]
    C --> D[Build]
    D --> E[RunAsync]
    E --> F[应用执行]
1
2
3
4
5
6
flowchart TD
    A[CreateApplicationBuilder] --> B[创建Host Builder]
    B --> C[注册服务]
    C --> D[Build]
    D --> E[RunAsync]
    E --> F[应用执行]

使用依赖注入

Host默认集成依赖注入(Dependency Injection, DI)。例如定义一个服务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public interface ICameraService
{
    void Capture();
}

public class CameraService : ICameraService
{
    public void Capture()
    {
        Console.WriteLine($"{nameof(CameraService)} capture image");
    }
}

注册并使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<ICameraService, CameraService>();

var host = builder.Build();

var cameraService = host.Services.GetRequiredService<ICameraService>();
cameraService.Capture();

await host.RunAsync();

服务生命周期

.NET DI提供三种生命周期:Singleton、Transient和Scoped。

Singleton

整个应用程序只有一个实例,一般用于资源对象。

1
builder.Services.AddSingleton<ICameraService, CameraService>();

适合PLC连接、相机管理、轴卡控制、数据库连接等。

Transient

每次获取创建新实例,一般用于计算对象。

1
builder.Services.AddTransient<ImageProcessor>();

适合无状态、轻量对象,比如算法处理器、数据转换器、参数验证器等。

Scoped

一个Scoped一个实例,主要用于ASP.NET Core请求,工业软件中使用较少。

使用BackgroundService创建后台服务

工业软件大量存在后台循环任务。例如:PLC读取IO,更新状态。可以使用BackgroundService

创建:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class PlcMonitorService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            ReadPLC();
            
            await Task.Delay(1000, stoppingToken);
        }
    }

    private void ReadPLC()
    {
        Console.WriteLine($"{nameof(PlcMonitorService)} reading PLC {DateTime.Now}");
    }
}

注册:

1
builder.Services.AddHostedService<PlcMonitorService>();

启动后:

图表 代码
flowchart TD
    A[Host启动] --> B[PlcMonitorService启动]
    B --> C[ExecuteAsync]
    C --> D[循环读取PLC]
flowchart TD
    A[Host启动] --> B[PlcMonitorService启动]
    B --> C[ExecuteAsync]
    C --> D[循环读取PLC]
flowchart TD
    A[Host启动] --> B[PlcMonitorService启动]
    B --> C[ExecuteAsync]
    C --> D[循环读取PLC]
1
2
3
4
flowchart TD
    A[Host启动] --> B[PlcMonitorService启动]
    B --> C[ExecuteAsync]
    C --> D[循环读取PLC]

配置管理

不要在源代码中将参数写死:

1
string ip = "192.168.1.10";

推荐创建appsettins.json文件:

定义:

1
2
3
4
5
public class PlcOptions
{
    public string IP { get; set; }
    public int Port { get; set; }
}

注册:

1
builder.Services.Configure<PlcOptions>(builder.Configuration.GetSection("PLC"));

使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class PlcMonitorService(IOptions<PlcOptions> options) : BackgroundService
{
    private readonly PlcOptions options = options.Value;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            ReadPLC();
            
            await Task.Delay(1000, stoppingToken);
        }
    }

    private void ReadPLC()
    {
        Console.WriteLine($"{nameof(PlcMonitorService)} reading PLC {DateTime.Now} IP:{options.IP} PORT: {options.Port}");
    }
}

日志

Host默认提供日志系统。

使用:

1
2
3
4
5
6
7
8
9
public class CameraService(ILogger<CameraService> logger) : ICameraService
{
    private readonly ILogger<CameraService> logger = logger;

    public void Capture()
    {
        logger.LogInformation("Starting camera capture");
    }
}

Microsoft.Extensions.Hosting默认输出是控制台和Debug窗口,如果需要将其保存到文件中,常用方案是SerilogNloglog4net。目前.NET工程中Serilog + Microsoft.Extensions.Hosting是最常见的组合。

安装Serilog

安装NuGet包:

1
2
3
4
5
dotnet add package Serilog.Extensions.Hosting

dotnet add package Serilog.Sinks.File

dotnet add package Serilog.Settings.Configuration

作用:

作用
Serilog.Extensions.Hosting 集成 Host
Serilog.Sinks.File 输出文件
Serilog.Settings.Configuration 读取配置

配置appsettings.json

创建appsettings.json(如果项目中没有该文件的话),加入:

Host中接入Serilog

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using Serilog;


var builder =
    Host.CreateApplicationBuilder(args);


// 使用Serilog替换默认Logger
builder.Services
    .AddSerilog(
        new LoggerConfiguration()
        .ReadFrom.Configuration(
            builder.Configuration)
        .CreateLogger()
    );


builder.Services
    .AddSingleton<PlcMonitorService>();


var host =
    builder.Build();


await host.RunAsync();

每天会生成新的日志文件:

图2 日志文件
图2 日志文件

如果想Serilog同时在Console和Debug Window输出,需要安装另外两个NuGet包:

1
2
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.Debug

appsettings.json修改为:

Host生命周期管理

Host可以统一管理应用程序从启动、运行到关闭的完整生命周期。

一个Host的生命周期如下:

图3 Host生命周期
图3 Host生命周期

核心组件:

组件 作用
IHostedService 定义生命周期
BackgroundService 后台循环任务
StartAsync 启动初始化
ExecuteAsync 持续运行
StopAsync 停止释放
CancellationToken 优雅退出
IHostApplicationLifetime 主动控制生命周期

IHostApplicationLifetime是.NET Generic Host中用于控制应用程序生命周期的核心接口,其定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface IHostApplicationLifetime
{
    CancellationToken ApplicationStarted { get; }

    CancellationToken ApplicationStopping { get; }

    CancellationToken ApplicationStopped { get; }

    void StopApplication();
}
成员 作用
ApplicationStarted Host启动完成
ApplicationStopping 准备关闭
ApplicationStopped 已经关闭
StopApplication() 主动停止Host

例如,我们在ApplicationStarted后执行一些初始化操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class DeviceInitializeService(IHostApplicationLifetime applicationLifetime, ILogger<DeviceInitializeService> logger) 
    : IHostedService
{
    private readonly IHostApplicationLifetime appLifetime = applicationLifetime;
    private readonly ILogger<DeviceInitializeService> logger = logger;
    
    public Task StartAsync(CancellationToken cancellationToken)
    {
        appLifetime.ApplicationStarted.Register(() =>
        {
            _ = InitializeDevice();
        });
        
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    private async Task InitializeDevice()
    {
        logger.LogInformation("开始初始化");
        
        await Task.Delay(2000, appLifetime.ApplicationStopping);
        
        logger.LogInformation("结束初始化");
    }
}

使用:

1
2
3
4
5
6
7
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<DeviceInitializeService>();

var host = builder.Build();

await host.RunAsync();

WPF中使用Generic Host

App.xaml中移除StartupUri,改为:

1
2
3
4
5
6
7
8
<Application x:Class="WPFHostDemo.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WPFHostDemo">
    <Application.Resources>
         
    </Application.Resources>
</Application>

窗口不在由WPF创建,而是由Host掌控。在App.xaml.cs中创建Host:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public partial class App : Application
{
    public IHost Host { get; }

    public App()
    {
        var builder = Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder();
        ConfigureServices(builder.Services);
        Host = builder.Build();
    }

    private static void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<WPFHostDemo.MainWindow>();
    }
}

现在,Host已经成为WPF应用的根。

重写OnStartup以启动Host:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    protected override async void OnStartup(StartupEventArgs e)
    {
        await Host.StartAsync();
        
        var mainWindow = Host.Services.GetRequiredService<MainWindow>();
        
        mainWindow.Show();
        
        base.OnStartup(e);
    }

启动流程:

图表 代码
flowchart TD
    A[双击exe] --> B[WPF App启动]
    B --> C[创建Host]
    C --> D[注册服务]
    D --> E[启动后台服务]
    E --> F[创建MainWindow]
    F --> G[显示界面]
flowchart TD
    A[双击exe] --> B[WPF App启动]
    B --> C[创建Host]
    C --> D[注册服务]
    D --> E[启动后台服务]
    E --> F[创建MainWindow]
    F --> G[显示界面]
flowchart TD
    A[双击exe] --> B[WPF App启动]
    B --> C[创建Host]
    C --> D[注册服务]
    D --> E[启动后台服务]
    E --> F[创建MainWindow]
    F --> G[显示界面]
1
2
3
4
5
6
7
flowchart TD
    A[双击exe] --> B[WPF App启动]
    B --> C[创建Host]
    C --> D[注册服务]
    D --> E[启动后台服务]
    E --> F[创建MainWindow]
    F --> G[显示界面]

WPF退出时,关闭Host:

1
2
3
4
5
6
    protected override async void OnExit(ExitEventArgs e)
    {
        await Host.StopAsync();
        Host.Dispose();
        base.OnExit(e);
    }

关闭流程:

图表 代码
flowchart TD
    A[用户关闭窗口] --> B[Host.StopAsync]
    B --> C[停止Worker]
    C --> D[释放资源]
    D --> E[程序退出]
flowchart TD
    A[用户关闭窗口] --> B[Host.StopAsync]
    B --> C[停止Worker]
    C --> D[释放资源]
    D --> E[程序退出]
flowchart TD
    A[用户关闭窗口] --> B[Host.StopAsync]
    B --> C[停止Worker]
    C --> D[释放资源]
    D --> E[程序退出]
1
2
3
4
5
flowchart TD
    A[用户关闭窗口] --> B[Host.StopAsync]
    B --> C[停止Worker]
    C --> D[释放资源]
    D --> E[程序退出]

这里的停止Worker指的是后台工作服务(Background Worker),也就是实现了IHostService的后台服务。

总结

Microsoft.Extensions.Hosting提供了一套现代 .NET 应用架构模式:DI + Configuration + Logging + IHostedService。它解决的问题:

问题 解决方案
对象创建混乱 DI
生命周期管理 Host
后台任务 IHostedService
配置管理 Options
日志管理 ILogger
模块耦合 接口抽象

对于传统桌面软件、工业设备软件、自动化控制系统,这种结构可以很好地支撑从单机设备向复杂自动化平台演进。


相关内容

请作者喝杯咖啡!
AndyFree96 支付宝支付宝
AndyFree96 微信微信