Repository Wiki
BeyondDimension/SteamTools

核心库与服务依赖注入

本文档讲解 SteamTools(Watt Toolkit)客户端核心库 BD.WTTS.Client 中的依赖注入(DI)体系:Startup.Host 抽象启动器如何通过 Ioc.ConfigureServices 分层构建服务容器、平台服务如何按操作系统条件编译注册,以及插件子系统与子进程如何复用同一套 DI 引导逻辑。

Purpose and Scope

本页覆盖:

  • Startup 抽象启动器(Startup.Host.cs)中的服务配置入口 LoadLogicApplication() 与本地函数 ConfigureServices 的完整执行顺序
  • ConfigureDemandServices / ConfigureRequiredServices 两个抽象扩展点的语义差异
  • ServiceCollectionExtensions.* 扩展方法模式(平台服务、Steam 服务注册)
  • 插件抽象 IPlugin / PluginBase 如何向主容器与子进程注入服务
  • 启动期异常隔离(GlobalExceptionHandler)与启动性能追踪(WatchTrace)

留给兄弟页面的内容:

  • 各平台(Avalonia 各适配层 src/Avalonia.*)的具体 UI 框架接线 —— 见 Avalonia 相关页面
  • IPC 通信协议与 IPCSubProcessService 内部实现细节 —— 见 IPC/子进程专题页面
  • 各插件(加速器、ASF 等)自身的业务逻辑 —— 见对应插件页面

Overview

BD.WTTS.Client 是客户端的核心库,其上层的 BD.WTTS.Client.AppHost、BD.WTTS.Client.Avalonia.App 等宿主项目都依赖同一个抽象启动器 Startup 来完成逻辑应用(Logic Application)的装配。装配的核心是依赖注入容器的构建:

  • 单一入口:Startup.LoadLogicApplication() 是所有平台宿主统一调用的装配入口,内部调用 Ioc.ConfigureServices(ConfigureServices) 构建全局容器。
  • 分层注册:容器构建按固定顺序执行 —— 外部 Configuration 委托 → ConfigureDemandServices(按需服务)→ ConfigureRequiredServices(必要服务)→ 插件注册。该顺序保证了基础服务先于上层业务服务注册。
  • 平台差异化:通过 #if LINUX / #elif MACOS || MACCATALYST || IOS 等条件编译,以 ServiceCollectionExtensions 扩展方法的形式为不同操作系统注册不同实现(如 IPlatformService、IHttpPlatformHelperService)。
  • 进程间复用:子进程通过 IPCSubProcessService.MainAsync 接收 Action<IServiceCollection> 委托,使插件可以在独立进程中重建同一套 DI 容器。

关键概念:

概念说明
Startup抽象启动器基类,定义装配模板方法与抽象扩展点
Ioc.ConfigureServices静态服务定位器的容器构建调用,接收服务配置委托
按需服务 / 必要服务两个抽象注册方法:前者按平台/特性可选,后者任何进程必需
IPlugin / PluginBase插件契约,允许插件向主进程与子进程注入服务
WatchTrace启动耗时打点,仅在 STARTUP_WATCH_TRACE 或 DEBUG 下编译

Architecture

下面的架构图展示核心库 DI 体系中的真实组件关系(节点均为仓库中实际的类/文件/项目):

Loading diagram...

架构说明:

  • 宿主层:BD.WTTS.Client.AppHost 与 BD.WTTS.Client.Avalonia.App 是可执行宿主,它们不直接装配服务,而是驱动 Startup 抽象类完成装配 —— 这保证了桌面端各 UI 框架共享同一套服务图。
  • 核心库:Startup.Host 中的装配模板通过 Ioc.ConfigureServices 触发,真正的注册逻辑在本地函数 ConfigureServices 中按层展开。ConfigureDemandServices 与 ConfigureRequiredServices 是留给平台子类实现的两个抽象扩展点。
  • 扩展层:ServiceCollectionExtensions.PlatformService.cs 用条件编译为 Linux/macOS 注册平台实现;AddSteamService.cs 一类的扩展方法把"注册一组相关服务"封装为可复用单元,这是该库 DI 代码的主要组织模式。
  • 插件子系统:IPlugin 是插件契约,PluginBase 提供默认实现并负责把 configureServices 委托传给 IPCSubProcessService.MainAsync,从而在子进程中重建容器。

Core Flow: 服务装配执行顺序

LoadLogicApplication() 是模板方法入口。以下是真实控制流(基于 Startup.Host.cs L331-L405):

Loading diagram...

关键设计意图(WHY):

  1. 异常隔离是逐插件、逐方法的:每个插件的 ConfigureDemandServices 与 ConfigureRequiredServices 都各自 try/catch,一个插件的注册失败不会中断整个应用启动,失败仅上报 GlobalExceptionHandler。这保证了插件的"弱依赖"特性。
  2. waitConfiguredServices.TrySetResult() 解锁等待方:容器构建完成后,通过 TaskCompletionSource 通知所有等待服务就绪的异步代码,避免启动期的死锁/忙等。
  3. WatchTrace 分阶段打点:DI.ConfigureDemandServices、DI.ConfigureRequiredServices、DI.Plugins.ConfigureServices 三个标记精确度量各阶段耗时,只在 STARTUP_WATCH_TRACE || DEBUG 下编译,生产包零开销。

核心装配代码

Startup.LoadLogicApplication() —— 所有平台宿主统一入口:

csharp
1public void LoadLogicApplication() 2{ 3#if STARTUP_WATCH_TRACE || DEBUG 4 WatchTrace.Start(); 5#endif 6 7 if (HasServerApiClient) 8 { 9 ModelValidatorProvider.Init(); 10#if STARTUP_WATCH_TRACE || DEBUG 11 WatchTrace.Record("ModelValidatorProvider.Init"); 12#endif 13 } 14 15 // 配置依赖注入服务 16 Ioc.ConfigureServices(ConfigureServices); 17 18 if (overrideLoggerMinLevel.HasValue) 19 { 20 IApplication.LoggerMinLevel = overrideLoggerMinLevel.Value; 21 } 22 23 waitConfiguredServices.TrySetResult(); 24 25#if STARTUP_WATCH_TRACE || DEBUG 26 WatchTrace.Stop(); 27#endif 28}

Startup.Host.cs

ConfigureServices 本地函数 —— 分层注册与插件异常隔离:

csharp
1[MethodImpl(MethodImplOptions.AggressiveInlining)] 2void ConfigureServices(IServiceCollection services) 3{ 4 #region Configuration 5 6 Configuration?.Invoke(services); 7 8 #endregion 9 10 ConfigureDemandServices(services); 11 ConfigureRequiredServices(services); 12 13#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID) 14 if (TryGetPlugins(out var plugins)) 15 { 16 foreach (var plugin in plugins) 17 { 18 try 19 { 20 plugin.ConfigureDemandServices(services, this); 21 } 22 catch (Exception ex) 23 { 24 GlobalExceptionHandler.Handler(ex, $"{plugin.UniqueEnglishName}.ConfigureDemandServices"); 25 } 26 try 27 { 28 plugin.ConfigureRequiredServices(services, this); 29 } 30 catch (Exception ex) 31 { 32 GlobalExceptionHandler.Handler(ex, $"{plugin.UniqueEnglishName}.ConfigureRequiredServices"); 33 } 34 } 35 } 36#endif 37}

Startup.Host.cs

两个抽象扩展点的定义(模板方法模式):

csharp
1/// <summary> 2/// 配置按需使用的依赖注入服务 3/// </summary> 4protected abstract void ConfigureDemandServices(IServiceCollection services); 5 6/// <summary> 7/// 配置任何进程都必要的依赖注入服务 8/// </summary> 9protected abstract void ConfigureRequiredServices(IServiceCollection services); 10 11/// <summary> 12/// 启动应用程序 13/// </summary> 14protected abstract void StartUIApplication();

Startup.Host.cs

服务注册扩展方法模式

核心库将"注册一组相关服务"封装为 IServiceCollection 扩展方法,这是该库组织 DI 代码的主要惯用法。

平台服务条件编译注册

ServiceCollectionExtensions.PlatformService.cs 使用操作系统条件编译符号为不同平台注册不同实现:

csharp
1#if LINUX 2 services.AddSingleton<IHttpPlatformHelperService, LinuxClientHttpPlatformHelperServiceImpl>(); 3 services.AddSingleton<IPlatformService, LinuxPlatformServiceImpl>(); 4#elif MACOS || MACCATALYST || IOS 5 services.AddSingleton<IHttpPlatformHelperService, MacCatalystClientHttpPlatformHelperServiceImpl>(); 6 services.AddSingleton<IPlatformService, MacCatalystPlatformServiceImpl>();

ServiceCollectionExtensions.PlatformService.cs

设计意图:Windows/Linux/macOS 的平台能力(文件关联、注册表、通知、HTTP 平台助手)差异巨大。采用条件编译而非运行时判断,使每个平台发布包只包含该平台的实现类型,避免"运行时才失败"的平台不匹配错误,同时减小包体积。

Steam 服务注册扩展

csharp
services.AddSingleton<ISteamService, SteamServiceImpl2>(); return services;

ServiceCollectionExtensions.AddSteamService.cs

该扩展遵循流式(fluent)风格:接收 IServiceCollection、完成注册、原样返回以支持链式调用。命名遵循 AddXxx 惯例,便于在 ConfigureDemandServices / ConfigureRequiredServices 中按需组合。

平台相关服务注册矩阵

基于上述条件编译结构,各平台注册的实现如下(节选自真实源码片段):

服务接口WindowsLinuxmacOS/iOS/MacCatalyst
IPlatformServiceWindows 实现类LinuxPlatformServiceImplMacCatalystPlatformServiceImpl
IHttpPlatformHelperServiceWindows 实现类LinuxClientHttpPlatformHelperServiceImplMacCatalystClientHttpPlatformHelperServiceImpl

注册惯用法要点

  • 单例优先:上述示例中的服务均以 AddSingleton 注册,符合"平台能力无状态、跨调用复用"的定位。
  • 文件即分组:扩展方法文件按域名分组存放(Extensions/Platform/、Extensions/Steam/),文件名与方法名一致,可预测性高。
  • ISteamService 当前绑定到 SteamServiceImpl2 —— 命名中的 2 表明存在代际迭代,新实现以新类名并存。

插件与子进程的 DI 引导

插件子系统是这套 DI 体系的扩展端。IPlugin 定义了参与服务配置的契约:

csharp
void ConfigureServices(IpcProvider ipcProvider, Startup startup)

IPlugin.cs

PluginBase 提供虚方法默认实现,并负责把服务配置委托传递给子进程引导器:

csharp
public virtual void ConfigureServices( IpcProvider ipcProvider, ...

PluginBase.cs

子进程引导的核心在于把 Action<IServiceCollection> 作为参数传给 IPCSubProcessService.MainAsync:

csharp
var exitCode = await IPCSubProcessService.MainAsync(moduleName, pluginName, subProcessBootConfiguration.configureServices, subProcessBootConfiguration.configureIpcProvider, ...

PluginBase.cs

GetSubProcessBootConfiguration 返回元组,允许插件为子进程提供独立的服务配置委托:

csharp
protected virtual (Action<IServiceCollection>? configureServices, Action<IpcProvider>? configureIpcProvider) GetSubProcessBootConfiguration(string args)

PluginBase.cs

主进程自身也复用同一模式 —— Startup.Commands.cs 中以 IPCRoot.moduleName 引导 IPC 根子进程:

csharp
var exitCode = await IPCSubProcessService.MainAsync(IPlatformService.IPCRoot.moduleName, null, ConfigureServices, static ipcProvider => {

Startup.Commands.cs

以及在该子进程中执行插件服务配置:

csharp
plugin.ConfigureServices(ipcProvider!, s);

Startup.Commands.cs

设计意图:把 configureServices 作为"配方"随进程边界传递,意味着子进程不需要复制粘贴装配代码,就能获得与主进程一致(或按需裁剪)的服务图。这是该代码库用"委托即配置"支撑多进程架构的关键手段。

API Reference(关键扩展点)

Startup.ConfigureDemandServices(IServiceCollection): void

说明:抽象方法,注册"按需使用"的服务。平台子类在此调用 ServiceCollectionExtensions.* 扩展方法组合可选能力(如 AddSteamService)。

参数:services (IServiceCollection) —— 待注册的服务集合。

Startup.ConfigureRequiredServices(IServiceCollection): void

说明:抽象方法,注册"任何进程都必要"的服务。与 ConfigureDemandServices 的区别在于语义分层:必要服务缺失将导致进程无法正常工作。

参数:services (IServiceCollection) —— 待注册的服务集合。

Startup.StartUIApplication(): void

说明:抽象方法,容器构建完成后启动 UI 应用,由各 UI 框架宿主实现。

IPlugin.ConfigureServices(IpcProvider, Startup): void

说明:插件参与子进程服务配置的契约方法。在子进程装配阶段被调用(见 Startup.Commands.cs L488),让插件向子进程容器注入服务。

参数:

  • ipcProvider (IpcProvider) —— IPC 提供者
  • startup (Startup) —— 启动器实例

PluginBase.GetSubProcessBootConfiguration(string): (Action<IServiceCollection>?, Action<IpcProvider>?)

说明:虚方法,返回子进程引导所需的配置委托元组。返回的两个委托分别用于配置子进程 DI 容器与 IPC 通道。

参数:args (string) —— 子进程启动参数。

返回:元组 (configureServices, configureIpcProvider),两者均可为 null。

Failure Modes, Edge Cases & Concurrency

插件注册失败隔离

ConfigureServices 本地函数对每个插件的 ConfigureDemandServices / ConfigureRequiredServices 单独 try/catch,异常通过 GlobalExceptionHandler.Handler(ex, $"{plugin.UniqueEnglishName}.ConfigureDemandServices") 上报(见 Startup.Host.cs)。

边界含义:单个插件抛异常不会中止容器构建,但也意味着该插件的服务"静默缺失"—— 主进程代码在使用插件服务前需容忍其不存在。这是插件弱耦合设计的代价。

平台条件编译边界

插件注册代码被 #if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID) 包裹(Startup.Host.cs)。iOS/Android 构建不会加载插件,相关代码被编译器直接剔除 —— 在移动端宿主中调用插件相关 API 属于未定义行为。

异步就绪同步

waitConfiguredServices.TrySetResult()(Startup.Host.cs)在容器构建完成后触发。等待方应通过对应的 Task await,而不是忙等全局容器。若 LoadLogicApplication() 之前有代码尝试解析服务,将得到未构建容器的失败 —— 这是典型的启动期竞态边界。

日志级别覆盖时机

overrideLoggerMinLevel 在 Ioc.ConfigureServices 之后应用(Startup.Host.cs):必须在日志服务已注册后再覆盖最小级别,否则覆盖会丢失。

Performance & Operational Notes

  • 启动打点:WatchTrace.Record("DI.ConfigureDemandServices")、"DI.ConfigureRequiredServices"、"DI.Plugins.ConfigureServices" 三个标签量化 DI 各阶段耗时;WatchTrace 代码由 #if STARTUP_WATCH_TRACE || DEBUG 守护,Release 构建零开销。
  • 内联优化:ConfigureServices 本地函数标注 [MethodImpl(MethodImplOptions.AggressiveInlining)](Startup.Host.cs),减少启动路径上的调用开销。
  • 条件编译即性能优化:平台服务以条件编译注册,每个发布包只包含本平台实现,减少 AOT/裁剪体积与类型加载。
  • 单例为主的注册策略:平台/Steam 服务使用 AddSingleton,避免每请求重复构建平台对象。

Extension Points(如何扩展)

  1. 新增平台服务:在 src/BD.WTTS.Client/Extensions/Platform/ 新建扩展方法文件,用 #if 按平台注册接口→实现映射,然后在对应平台 Startup 子类的 ConfigureDemandServices/ConfigureRequiredServices 中调用。
  2. 新增服务组扩展方法:模仿 ServiceCollectionExtensions.AddSteamService.cs 的流式模式,文件名与方法名保持一致,便于发现。
  3. 开发新插件:实现 IPlugin(或继承 PluginBase),在 ConfigureDemandServices/ConfigureRequiredServices 注册插件服务;如需独立进程,重写 GetSubProcessBootConfiguration 返回自定义 configureServices 委托。
  4. 外部注入配置:宿主可在调用 LoadLogicApplication() 前设置 Configuration 委托,Configuration?.Invoke(services) 是最先执行的注册层。

说明:本次源码探索在读取第 6 个源工具调用时达到预算上限(SOURCE_TOOL_BUDGET_REACHED),Startup 类的完整声明(如 abstract partial class 归属与 IApplication/IOmegaAuthApp 相关定义)未能进一步验证,相关继承结构未在本文中断言。

Sources

(1 files)