在传统.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整体架构
什么是Host?
Host可以理解为管理应用生命周期的运行环境 。类似Web Host管理Web Application,Docker Host管理Container。Host本身不负责业务逻辑。
Host负责:
创建服务
管理依赖
加载配置
初始化日志
启动后台服务
优雅关闭
业务代码负责:
创建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窗口,如果需要将其保存到文件中,常用方案是Serilog、Nlog或log4net。目前.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 日志文件
如果想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生命周期
核心组件:
组件
作用
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
模块耦合
接口抽象
对于传统桌面软件、工业设备软件、自动化控制系统,这种结构可以很好地支撑从单机设备向复杂自动化平台演进。