跳到主要内容
版本:3.1

程序集扫描与启动治理(Aegis.Core.Infrastructure)

从 3.1.4 起,AegisApplication.Run(args) 启动时只做一次程序集扫描,扫谁由白名单决定:解决方案里的业务项目,加上 Aegis 自家的包,再加上依赖了 Aegis 的 NuGet 包。第三方库不再进入扫描面,加载失败和版本冲突会直接打在启动横幅上。绝大多数项目一行配置都不用写;扫不到的场景用 Aegis:AssemblyScan 的 Include 兜底。

组件概览​

字段说明
功能名称程序集扫描与启动治理
真实类库Aegis.Core.Infrastructure(启动核心包,随核心包发布,不需要单独安装)
功能定位决定框架启动时"扫哪些程序集找类型",并提供扫描结果给全部组件消费
引入方式使用 AegisApplication.Run(args) 即有,配置节 Aegis:AssemblyScan 可省
关键配置appsettings.json 的 Aegis:AssemblyScan 节
关键能力白名单一次扫描、Include/Exclude 调节、加载失败与版本冲突告警、启动耗时画像、IAegisAssemblyProvider 统一出口

什么时候要用它​

先说不用动手的情况,这些默认就覆盖了:

  • 业务项目写在解决方案里(Component.deps.json 里 Type 为 project 的库)
  • 装了任意 Aegis.* 的 NuGet 包
  • 自家 NuGet 业务包里实现了 Aegis 的接口(ComponentRegister、IMessageConsumer、[Lifetime] 等),只要包引用了任何 Aegis 程序集,框架沿依赖图反向找得到,零配置接入

需要写配置的情况只有两类:

  • 运行时才加载的插件 DLL:不被项目引用、直接投放到输出目录,且名字不带 Aegis. 前缀
  • 包不依赖 Aegis、但里面有需要被发现的类型,比如只引用了微软 SignalR、没引用 Aegis.Net.SignalR 的 Hub 包

这两种都用 Include 追加。反过来,想把某个包从扫描面里踢出去(比如演示环境的 Aegis.Scheduler.Stub),用 Exclude。

默认扫描面:四路候选​

候选来源怎么判定覆盖什么
业务项目依赖清单里 Type == "project" 的库解决方案内的 Contract、Services、Repository 等项目
Aegis 前缀包库名或程序集名以 Aegis. 开头全部 Aegis NuGet 包
反向可达的包依赖图上直接或间接引用了任意 Aegis.* 库的 NuGet 包自家业务扩展包——实现了 Aegis 接口的包必然引用 Aegis 程序集,反向可达即纳入
磁盘 Aegis DLL输出目录下文件名匹配 Aegis.*.dlldrop-in 插件:不进依赖清单、直接拷进输出目录的 Aegis 前缀 DLL

四个来源取并集,再套用你配置的 Include / Exclude。第三方库(FreeSql、CAP、RabbitMQ.Client 这些)默认不进扫描面——框架要找的类型(组件注册器、启动器、消费者、Hub)只可能定义在前四路里,遍历它们是纯浪费。BaseDemo 实测:白名单 43 个程序集,扫描耗时 46ms,13 个组件全部注册成功。

回退路说明:单文件发布等场景下应用的 deps.json 可能缺失(DependencyContext 为 null),此时自动回退为磁盘全量扫描减去内置运行库前缀黑名单(Microsoft.、System.、mscorlib、netstandard、WindowsBase、Netty.、Autofac.、Accessibility,字面量前缀匹配),Include / Exclude 语义不变。

配置项说明​

配置节是 Aegis:AssemblyScan,两个字段都可省略:

字段类型作用
Includestring[]兜底追加进扫描面。给插件 DLL、不依赖 Aegis 但含需发现类型的包用
Excludestring[]从扫描面裁剪,优先级最高——最后执行,连 Include 加进来的也能裁掉

规则写法两种,程序集名匹配不区分大小写:

  • 精确名:Aegis.Scheduler.Stub
  • 前缀通配:Legacy.*,命中 Legacy. 开头的全部程序集,也命中叫 Legacy 的本体(与 NuGet 语义一致)
{
"Aegis": {
"AssemblyScan": {
"Include": [
"ThirdParty.Plugins.*",
"Company.HubPackage"
],
"Exclude": [
"Aegis.Scheduler.Stub",
"Legacy.*"
]
}
}
}

上面这段的意思:把 ThirdParty.Plugins 一族和 Company.HubPackage 追加进扫描面(它们原本不在四路候选里);再把 Aegis.Scheduler.Stub 和 Legacy 一族裁出去(即便它们命中了候选规则)。

配置先于扫描生效

Component.deps.json 和 appsettings.json 都加载完之后框架才开始扫描,所以 Aegis:AssemblyScan 写在 appsettings 或组件依赖文件里都行。3.1.4 之前扫描发生在配置加载前,配置根本来不及生效,这一版调整了顺序。

启动横幅怎么看​

启动时控制台会多出几段输出。扫描统计一行:

程序集扫描
[INFO] 已扫描程序集 43 个 · 类型 2531 项 · 耗时 46ms(Aegis.* + 业务项目白名单,Aegis:AssemblyScan 可调)

有加载失败或版本冲突时逐条 Warn,没有就不刷屏:

[WARN] 程序集加载失败 Company.Plugin.dll: 文件不存在且默认上下文无法解析
[WARN] 程序集 Aegis.Foo 部分类型加载失败,已跳过失败类型(首因: Could not load type 'X' from assembly 'Y')
[WARN] [版本冲突:ReferenceMismatch] Aegis.Core.Infrastructure: Aegis.Webapi.Services 编译时引用 Aegis.Core.Infrastructure 3.1.0,实际加载 3.1.4-preview1——已被统一为加载版本,若出现 MissingMethodException / TypeLoadException 优先核对此条

程序集加载失败在 3.1.4 之前是静默吞掉的——DLL 加载不了,类型直接从结果里消失,表现就是"有时扫不到"。现在每一条失败都有名有姓。类型枚举降级(ReflectionTypeLoadException)同理:能加载的类型照常参与,失败的类型跳过并记下首因。

应用启动完成后还有一段耗时画像:

[INFO] 启动耗时: 配置与扫描 61ms · 服务注册 198ms · 宿主构建与中间件 342ms · Initializers 9ms
[INFO] 最慢组件: Diagnostics (96ms), Logging (41ms), EventBus (23ms)

四个阶段对应 Run 的固定时序:配置与扫描、服务注册、宿主构建与中间件、Initializers。最慢 Top3 组件按注册耗时排序(同组件的 Service 和 Middleware 两阶段取更慢的那个)。启动慢时先看这段,框架侧扫描只占几十毫秒,大头通常在组件内部的连接和预热。如果注册期有组件日志,还会追加一行留存条数提示。

版本冲突告警​

只对 Aegis.* 程序集报警,第三方库的同名版本统一是 .NET 默认行为,报了也是噪音。两类:

类型含义建议动作
ReferenceMismatch某程序集编译时引用的 Aegis 版本与实际加载的不一致(NuGet 已统一到加载版本)不一定要改。运行正常可忽略;出现 MissingMethodException / TypeLoadException 时优先核对该条
ReferenceSplit多个程序集对同一个 Aegis 包要求不同版本,即使与加载版本一致也提示建议把各项目引用的 Aegis 包版本对齐,分裂是隐患

ReferenceSplit 的输出形态长这样(哪个版本被谁引用一目了然):

[WARN] [版本冲突:ReferenceSplit] Aegis.EventBus: 3.1.0 ← Aegis.Webapi.Base,Aegis.Webapi.Contract; 3.1.1 ← Aegis.Webapi.Services

组件作者:统一扫描出口​

3.1.4 起,框架组件不要再自己调 AppDomain.CurrentDomain.GetAssemblies()。启动扫描的结果统一从 IAegisAssemblyProvider(定义在 Aegis.Component)拿:

public interface IAegisAssemblyProvider
{
IReadOnlyList<Assembly> Assemblies { get; }

/// <summary>类型快照(一次扫描产出,已含 ReflectionTypeLoadException 降级),消费方不要再枚举 DefinedTypes。</summary>
IReadOnlyList<Type> Types { get; }
}

两个获取通道,对应两个生命周期阶段:

阶段获取方式示例(EventBus / SignalR 实际代码)
Service 注册(Build 前,无 IServiceProvider)AegisBuilder.Assembliesvar assemblies = builder.Assemblies?.Assemblies ?? AppDomain.CurrentDomain.GetAssemblies();
Middleware / 运行期(容器已构建)DI 单例 GetService<IAegisAssemblyProvider>()var provider = app.ApplicationServices.GetService<IAegisAssemblyProvider>();

Types 是扫描时一次产出的快照,已经处理过类型加载降级,直接用,不要再 GetTypes() 一遍。组件脱离 AegisApplication 宿主独立使用时出口为 null,回退 AppDomain 自扫即可(EventBus、SignalR、Diagnostics 的写法都是这个模式)。

自扫的问题在于时点:框架快照在 Run 起始,组件自扫在其后,AppDomain 惰性加载会让两边集合不一致,这是"有时扫不到"的另一个来源;重复扫描也白白多耗启动时间。

3.1.4 的语义变化​

升级前核对这几条:

变化旧行为新行为
drop-in DLL磁盘全量扫描,任意 DLL 投进输出目录都能被发现Aegis 前缀的照常发现;非 Aegis 前缀且不依赖 Aegis 的,默认不进扫描面,需 Include
第三方库全部被 Load 和遍历类型默认不进扫描面
加载失败静默吞掉启动横幅逐条 Warn

自查方法:升级后启动一次,对比横幅里的"已扫描程序集 N 个"和组件注册列表(检查遗漏服务 段会列出发现但未配置加载的组件)。组件数没少、业务功能正常,就说明扫描面收窄没有影响你;发现少了什么,把对应程序集名写进 Include。

ICustomStartup / IInitializer 的执行顺序也从这一版起确定(Order 升序,同序保持发现顺序),initializer 抛异常从启动崩溃变为 Fail 横幅后继续,详见基础设施。

接入后怎么确认生效​

  • 启动横幅出现"程序集扫描"段,程序集数/类型数/耗时三个数字都在
  • 组件注册列表与升级前一致(BaseDemo:13 个组件全部注册)
  • 没有 [版本冲突:...] 告警,或告警内容已知可接受
  • "启动耗时"一行四个阶段数字正常输出
  • 人为配错一个 Include 条目,能看到对应的加载失败 Warn(验证配置确实被读到)

常见问题​

问:升级后某个组件找不到了,怎么办?

先看横幅里"找不到组件 Xxx"或"当前未加载 Xxx"的提示,再看扫描统计的程序集数有没有明显变少。确认是扫描面问题后,把定义该组件的程序集名加进 Include,重启核对。

问:ReferenceMismatch 告警必须处理吗?

不必须。它说的是"编译时按 3.1.0 编的,运行时加载了 3.1.4-preview1"。NuGet 解析版本统一是正常机制,应用能跑就不用动。它的价值在于:哪天你见到 MissingMethodException 或 TypeLoadException,回来核对这条告警,十有八九就是版本差导致的。

问:为什么我的 Hub 包没被自动扫到?

Hub 类型不是 Aegis 契约,只引用微软 SignalR 的包不依赖任何 Aegis 程序集,反向可达判定够不着它。把包名写进 Include。

问:单文件发布后扫描行为一样吗?

单文件裁剪发布时应用 deps.json 缺失,框架自动回退为磁盘全量扫描减运行库黑名单前缀,Include / Exclude 规则照常生效。

问:Include 写了通配 Foo.*,会命中 Foo 这个程序集本身吗?

会。Foo.* 命中 Foo. 开头的所有程序集和名为 Foo 的本体,与 NuGet 包语义一致。

配套阅读​