OpenRobot.Framework.Modularity
模块化启动框架(net8.0):模块生命周期 + [DependsOn] 拓扑排序 + 程序集扫描 + 系统路径配置 + 后台定时任务。
日志基于 Microsoft.Extensions.Logging,依赖注入用标准 IServiceCollection。
- 模块化:一个模块 = 一个继承
StartupModule 的类;四阶段 PreInitialize → Initialize → PostInitialize → Shutdown,
外加异步就绪等待 WaitReadyAsync。依赖关系用 [DependsOn] 声明,框架做确定性拓扑排序。
- 构造注入:模块由容器经
ActivatorUtilities 创建,要日志注入 ILogger<TModule>,要服务注入接口——
没有静态服务定位器。
- 依赖只读:模块由容器创建、容器只构筑一次;不提供运行期改容器的能力。
- 无副作用:不写环境变量、不落盘、不起后台线程(后台任务要显式注册才会跑)。
安装
<PackageReference Include="OpenRobot.Framework.Modularity" Version="1.0.0" />
包由本仓库产出:dotnet pack common/OpenRobot.Framework.Modularity/OpenRobot.Framework.Modularity.csproj -c Release。
依赖:
Microsoft.Extensions.DependencyInjection 8.0.1(完整包:BuildServiceProvider() 不在 Abstractions 里)
Microsoft.Extensions.Logging.Abstractions 8.0.2
库不引具体日志实现。控制台输出由宿主引 Microsoft.Extensions.Logging.Console 并调 AddSimpleConsole() 决定。
快速开始
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenRobot.Framework.Modularity;
using OpenRobot.Framework.Modularity.BackgroundTasks;
using OpenRobot.Framework.Modularity.Paths;
var builder = ModuleHostBuilder.Create(args);
builder.Services.AddLogging(b => b
.AddSimpleConsole(o => { o.SingleLine = true; o.TimestampFormat = "HH:mm:ss.fff "; })
.SetMinimumLevel(LogLevel.Information));
builder.Services.AddSingleton<ISystemPathConfiguration>(new SystemPathConfiguration());
builder.Services.AddSingleton<IBackgroundTaskManager, BackgroundTaskManager>();
builder.Services.AddSingleton<MyService>();
builder.Options.AssemblyPrefixes.Add("MyApp"); // 主动加载 MyApp*.dll 里的模块
builder.AddModule<BackgroundTasksModule>(); // 想要后台任务统一启停就加这一行
await using var host = builder.Build(); // 容器在这里构筑,且只构筑这一次
await host.RunAsync(); // 初始化 → 就绪等待 → 阻塞至 Ctrl+C → 逆序关闭
写一个模块
public sealed class MyModule : StartupModule
{
private readonly ILogger<MyModule> _logger;
private readonly MyService _service;
// 构造注入:容器负责解析;模块本身不必注册进 services
public MyModule(ILogger<MyModule> logger, MyService service)
{
_logger = logger;
_service = service;
}
public override void Initialize()
{
// 只注册、不连接——此刻其它模块的处理器可能还没就位
_service.MessageReceived += OnMessage;
_logger.LogInformation("MyModule 已装配");
}
public override void PostInitialize()
{
_service.Connect(); // 全部模块 Initialize 完成后才发起连接
}
public override async Task WaitReadyAsync(CancellationToken ct)
{
await _service.WaitConnectedAsync(ct); // 就绪前不打印「服务已启动」
}
public override void Shutdown() => _service.MessageReceived -= OnMessage;
}
声明依赖
[DependsOn(typeof(DeviceRegisterModule))] // 本模块排在 DeviceRegisterModule 之后
public sealed class CloudMqttModule : StartupModule { /* ... */ }
API 速查
ModuleHostBuilder
| 成员 |
说明 |
Create(args) |
创建构建器;args 存进 Options.Arguments,库不解析 |
Services |
IServiceCollection,用标准扩展方法注册 |
Options |
ModuleHostOptions(扫描前缀 / 就绪超时 / Ctrl+C) |
AddModule<TModule>() |
显式注册模块类型(不在扫描范围或需要固定顺序时用) |
Build() |
构筑容器 + 收集模块(显式 ∪ 扫描)→ 拓扑排序 → 实例化,返回 ModuleHost |
ModuleHost
| 成员 |
说明 |
Services / Modules |
容器 / 按拓扑序的模块实例 |
Initialize() |
PreInitialize → Initialize → PostInitialize,幂等 |
WaitReadyAsync(ct) |
按序调各模块就绪等待;单个超时/异常不阻断其余 |
RunAsync(ct) |
完整流程:初始化 → 就绪 → 阻塞至 Ctrl+C / ct → 关闭 |
Shutdown() |
逆序关闭,逐模块隔离异常,幂等 |
DisposeAsync() |
Shutdown + 释放容器 |
ModuleHostOptions
| 项 |
默认 |
说明 |
AssemblyPrefixes |
空 |
扫描前主动加载的 dll 名前缀(不区分大小写)。空 = 只扫已加载程序集 |
ReadyTimeout |
15s |
就绪等待整体超时;超时后后台初始化继续 |
HandleConsoleCancelKeyPress |
true |
RunAsync 是否接管 Ctrl+C |
Arguments |
启动参数 |
原样保存,库不解析 |
StartupModule
| 成员 |
说明 |
PreInitialize / Initialize / PostInitialize / Shutdown |
四个虚方法,默认空实现 |
WaitReadyAsync(ct) |
异步就绪等待,默认立即就绪 |
Services |
宿主容器。优先用构造注入,本属性只是延迟解析的逃生口 |
BackgroundTasks
| 类型 |
说明 |
IBackgroundTask |
Name + Start() + IAsyncDisposable |
BackgroundTaskBase |
周期任务基类:启动即执行一次,之后按 Interval 循环;异常不中断循环 |
IBackgroundTaskManager |
All / Register(不启动)/ StartAll(幂等)/ StopAll(逆序 + 清空) |
BackgroundTasksModule |
内置模块:PostInitialize 统一启动、Shutdown 统一停止 |
public sealed class HeartbeatTask : BackgroundTaskBase
{
public HeartbeatTask(ILoggerFactory factory, MyService service) : base(factory) => _service = service;
public override string Name => "心跳";
public override TimeSpan Interval => TimeSpan.FromSeconds(30);
protected override Task ExecuteAsync(CancellationToken ct) => _service.PingAsync(ct);
}
// 模块 Initialize 里注册,BackgroundTasksModule 在 PostInitialize 统一启动
_manager.Register(new HeartbeatTask(loggerFactory, service));
Paths
| 成员 |
值 |
AppRootPath |
构造参数;默认 AppContext.BaseDirectory |
AppDataPath / AppLogPath / PluginPath |
{root}/Data、{root}/Logs、{root}/Plugins |
与源工程对照
迁移自 SmartEdge 的 OpenRobot.EdgeControl.* / SmartStudio.SelfHost.Startup:
| 源工程 |
本库 |
变化 |
StartupModule |
StartupModule |
去掉 Configuration 属性(改构造注入) |
SmartStartup |
ModuleHost + ModuleHostBuilder |
静态类 → 实例类;扫描前缀从硬编码改为可配 |
ISmartLogger / ConsoleLogger |
删除 |
换 Microsoft.Extensions.Logging |
SmartConfiguration |
删除 |
换标准 DI |
StartupModuleConfig |
删除 |
「日志 + 系统路径」打包体,两者现由 DI 直接提供 |
HostStartupModule |
BackgroundTasksModule |
同样职责,收进库内 |
SystemPathConfiguration |
Paths.SystemPathConfiguration |
增加可注入的根目录(便于测试) |
三处行为差异(有意为之)
- 循环依赖抛异常:源实现检测到环后把剩余模块按发现顺序追加(顺序随机且无人知晓)。
本库在
Build() 抛 InvalidOperationException,消息含涉及模块名。
- Shutdown 逐模块隔离:源实现任一模块抛异常即中断逆序链,后续模块永不释放。本库逐个 try/catch 记日志继续。
- Initialize 失败回滚:源实现
Initialize() 抛出后已初始化模块不回收。本库捕获后逆序 Shutdown
已进入 Initialize 的模块再重抛,不留半初始化状态。
确定性拓扑排序
源工程按 AppDomain.GetAssemblies() 的返回顺序建图——同层模块的先后由运行时决定,多机部署可能出现
「本机正常、另一台启动顺序不同」。本库先把节点按 (程序集名, 类型全名) 定序再排序,相同输入永远给出相同顺序。
已知限制
- 容器必须异步释放:
ModuleHost 内含只实现 IAsyncDisposable 的服务时,同步 Dispose() 会抛
InvalidOperationException。始终写 await using var host = builder.Build();。
- 容器构筑后冻结:不提供运行期增删服务的能力(源工程
SmartConfiguration 的反复重建容器会导致
旧 provider 泄漏、已解析单例丢失)。运行期可变状态请自己持有字段或用模块间事件。
[DependsOn] 指向未参与构建的类型会被忽略:否则引一个可选模块就得连带引它的整个依赖程序集。
- 框架自带模块不参与自动扫描:
BackgroundTasksModule 位于框架程序集,扫描时被跳过——
自动纳进来会让「没注册 IBackgroundTaskManager」的宿主在 Build() 里直接抛(编译通过、启动崩溃)。
要用后台任务统一启停,请显式 AddModule<BackgroundTasksModule>() 并注册 IBackgroundTaskManager。
扫描的语义是「发现应用自己的模块」,与源工程一致(HostStartupModule 本就在应用程序集里)。
PreInitialize 阶段不参与回滚:若某模块在 PreInitialize 抛出,已执行 PreInitialize 的模块不会被
Shutdown(它们的 Initialize 尚未开始,通常也没有资源在手)。需要资源保护的初始化放 Initialize。
版本历史
1.0.0(首次发布)
- 模块生命周期
PreInitialize / Initialize / PostInitialize / WaitReadyAsync / Shutdown
[DependsOn] 确定性拓扑排序,循环依赖抛异常
ModuleHostBuilder / ModuleHost:容器一次性构筑、模块经 ActivatorUtilities 构造注入
- 程序集扫描(可配前缀,主动加载未加载的 dll;
ReflectionTypeLoadException 降级取可用类型)
ISystemPathConfiguration / SystemPathConfiguration
BackgroundTaskBase / IBackgroundTaskManager / BackgroundTasksModule
- 日志改用
Microsoft.Extensions.Logging,不再有自定义日志抽象