跳到主要内容
版本:3.1

Component.deps.json 组件清单

每个 Aegis 应用都有两份绕不开的配置:appsettings.json 管行为参数,Component.deps.json 管装哪些组件。要日志、要跨域、要认证鉴权,做法都是把组件名写进这份清单,框架启动时照单装配。

这份文件列出要装的组件。装配顺序由框架根据依赖声明(DependsOn)和优先级(Order)排定,清单里数组的先后写法不影响顺序。同一套组件在任何机器、任何环境跑出来的「执行顺序」横幅逐字相同。想先把项目跑起来,看快速开始第 2 步;这一页把整份文件的规则讲全。

这是一份什么文件​

一份 JSON 集合声明,放在项目里、随构建复制到运行目录,AegisApplication.Run(args) 启动时读取。最小可用的样子:

{
"Components": {
"Load": [
"Logging",
"BusinessServices",
"RequestValidation",
"Swagger"
]
}
}

四行 Load 完成四件事:接日志、注册业务服务、开 Swagger、启用请求验证。文件要跟着构建走,csproj 里配复制(和 NLog.config 同样的做法):

<ItemGroup>
<Content Update="Component.deps.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
事项行为
文件放哪项目根目录,复制到运行目录(上表 csproj 配置)
谁读它AegisApplication.Run(args),启动期一次性解析
文件不存在 / 没有 Components 节启动直接失败,报错带指引(旧版会静默继续、晚些在注册深处抛异常)
三个键全缺按空清单处理,不算错误
想按环境换整份清单appsettings 里配 ComponentDeps 指定文件名,见下文

三个键怎么写:按阶段并集​

Components 节下三个键,全部是集合:

键管哪个阶段说明
Load两个阶段共用公共基底,服务注册和中间件挂载都用它
Services只管服务阶段在 Load 之上补充,只影响服务注册
Middlewares只管中间件阶段在 Load 之上补充,只影响中间件挂载

解析就两条公式:服务阶段 S = Load ∪ Services,中间件阶段 M = Load ∪ Middlewares。三个键写几个都行,写了就算数,彼此是并集关系,不存在谁覆盖谁。

推荐写法:只写 Load​

日常接入写 Load 一份就够。下面是 BaseDemo 的真实清单(节选):

{
"Components": {
"Load": [
"UMeta.Logging",
"Cors",
"IdGenerator",
"BusinessServices",
"RequestValidation",
"EventBus",
"ConfigCenter",
"Jobs",
"Diagnostics",
"Swagger"
]
}
}

纯服务组件(EventBus、Jobs、IdGenerator 这类没有中间件的)在中间件阶段自动跳过,Load 里不需要为它们做区分。

高级写法:按阶段补充​

要让某个组件只注册服务、不挂中间件(或反过来),用 Services / Middlewares 分开写。典型场景是双认证源(来自 demo 的 Component.deps.AuthDual.json):

{
// Middlewares 含 EssAuthorize:ESS 生效时 ClientTokenHandler 挂载、被排除时不挂载
"Components": {
"Services": [
"UMeta.Logging",
"Swagger",
"Cors",
"Authentication",
"SsoAuthorize",
"EssAuthorize"
],
"Middlewares": [
"UMeta.Logging",
"Swagger",
"Cors",
"EssAuthorize"
]
}
}

Authentication 只在 Services 里,所以只注册认证服务、不挂它自带的 TokenHandler;请求校验由 EssAuthorize 的 ClientTokenHandler 处理。测试环境用 SSO、正式环境用 ESS,靠 appsettings 侧的排除配置切换,见按环境排除组件。

不写 Load、只写 Services + Middlewares 两份列表,是旧版格式,继续支持、没有弃用计划,存量项目不用改。

两个必知的坑​

数组顺序没有任何含义

清单是纯集合:先后、换行、分组都不影响装配顺序。想调顺序,去改组件的 Order 或补 DependsOn 声明(组件作者侧的事,见下文),改 JSON 里的书写顺序没有用。

空数组绑定为 null,等同未写

.NET 配置绑定把 "Services": [] 绑定为 null,与不写这个键效果一样。所以想用空数组把某个阶段「清空」做不到——Load ∪ 空集 还是 Load 本身。确实要某个阶段不装配,用 Aegis:Components:Disabled 排除,或者用 ComponentDeps 换一份文件。

顺带两条集合语义:组件名跨键、键内重复都会去重,装配一次;同名组件存在多个实现类时保留首个并 Warn(那是组件实现的重复,不是你清单写重了)。

依赖自动装配:服务阶段闭包​

服务注册讲究完备:组件依赖谁的服务,谁的服务就得在容器里。3.1.4 起服务阶段沿 DependsOn 声明补齐依赖,清单缺了谁就自动拉入装配。

最直接的例子:清单只写 EssAuthorize,Authentication 和 Authorization 的服务会自动装配,因为 EssAuthorize 声明了依赖这两者。启动横幅会告诉你拉了什么:

组件装配解析
自动加载 Authentication(被 EssAuthorize 依赖)
自动加载 Authorization(被 EssAuthorize 依赖)

多级依赖链同样传递处理:依赖的依赖也会拉。被拉入的组件按它自己的 Order 和名字落位。

中间件阶段不做闭包。 中间件挂不挂只看 Middlewares 键:键里有谁,谁挂中间件。上面双认证源的例子里,Authentication 的服务被拉入,它的 TokenHandler 不挂——请求校验已经由 EssAuthorize 的 ClientTokenHandler 负责。

装配顺序:框架怎么排​

装配序由三层规则排定,前面的层压住后面的层:

层规则性质
1DependsOn:被依赖组件必在前硬约束,Order 不能越过它
2Order:数值小者在前软优先级,只在没有依赖关系时决定相对位置
3组件名字典序前两层分不出先后时定序,保证跨环境结果一致

没有依赖关系的组件之间比 Order;Order 相同就按名字 Ordinal 字典序。上面示例清单的实际执行顺序:

ConfigCenter → UMeta.Logging → Cors → Swagger → BusinessServices → Diagnostics
→ EventBus → IdGenerator → Jobs → RequestValidation

对照源清单:ConfigCenter 在 JSON 里写在第 7 位,实际执行排最前——它 Order 是 -250(配置源层),后面的组件才读得到远端配置;Swagger 排在 Cors 之后、业务组件之前,开启认证后不带 token 也能打开文档页。书写位置完全不影响结果。

Order 锚点体系​

ComponentRegister.Order 默认 10000:不覆写就排在所有锚点组件之后,同默认值的组件之间按字典序。全框架只有 9 个组件覆写了 Order,覆写值本身就是位置语义:

Order层组件为什么必须靠前
-250配置源ConfigCenter、SystemSettings注册期即时添加配置源,排在后面的组件才读得到远端配置
-200遥测日志Logging、UMeta.Logging日志管道要包住全管线(含认证层)才能完整计时打点
-100入口Cors、Prometheus预检请求不带鉴权头,跨域必须排在认证前;计量要覆盖全管线
-60认证前可访问Swagger开启认证后 TokenHandler 对无凭据请求直接返回 401,Swagger 文档页不带 token 也要能打开,所以排在认证组件之前
-50认证AuthenticationTokenHandler 校验所有请求,必须排在业务中间件之前
-40鉴权AuthorizationUseAuthorization 在认证之后——先后顺序直接写进数字
10000默认值其余全部组件同值组件按字典序;有依赖声明的由 DependsOn 保证先后

业务组件的判断标准:没有管线位置要求就不覆写 Order,排最后不影响功能;需要排在某个特定组件之前(比如必须在认证前读请求头),覆写一个比那个组件更小的值。0 到 9999 之间随便取,框架锚点都在这个区间外,不会撞上。

Order 只能写在组件代码里

Order 是 ComponentRegister 上的虚属性,由组件作者声明,服务注册与中间件挂载共用同一个值。deps.json 不提供顺序覆盖——顺序知识和组件实现放在一起;配置侧开放顺序,就退回手排老路了。

注意区分同名概念:ICustomStartup.Order 是业务启动器的执行优先级(默认 0,多个 Startup 之间排序),和 ComponentRegister.Order(组件装配顺序,默认 10000)只是名字相同,量纲和默认值都不同,互不相干。详见Startup。

DependsOn 声明契约​

DependsOn 声明在组件代码里,语义是必需——服务阶段闭包会把它拉进装配,没有「声明了但不装」的中间态:

public class ServiceCollectionExtensions : ComponentRegister
{
public override string ComponentName => "EssAuthorize";

// 必需依赖:框架自动加载并保证先装配
public override string[] DependsOn => new[] { "Authentication", "Authorization" };
}

两条契约:

  • 可选依赖不要声明。DependsOn 是硬性的「没它不行」,闭包会把声明的依赖全部拉入。可选协作(比如四后端文件存储那种平级互斥关系)不该走 DependsOn。
  • Order 方向一致:Order(被依赖组件) ≤ Order(依赖方)。相等合法——Jobs.Postgres 依赖 Jobs,两者都是默认值 10000,拓扑边保证 Jobs 先装配,同值不冲突。方向反了属于元数据自相矛盾,CI 架构测试会拦。

框架内部还有程序集引用图与 DependsOn 的交叉校验兜底:组件程序集引用了另一个组件却不声明依赖(也没有白名单豁免),CI 直接失败。漏声明不会静默漏装。

按环境排除组件​

先看一个会真实遇到的问题。应用接了两套认证:内网部署走公司统一 SSO,云上部署走 ESS 票据认证。两套互斥,同一时刻只能生效一套。deps.json 随程序集打包、所有环境共用一份,写谁不写谁?

一个办法是维护两份清单文件,部署时各环境带上各自那份。这个办法能跑,代价是两份清单的其余内容要人工保持一致——新增一个 EventBus 得记得两边都加,漏一边就是某个环境悄悄缺功能,这类问题排查起来很费时间。

Aegis:Components:Disabled 就是为这种环境差异准备的:清单保持一份全量(两套认证都写进去),环境差异交给 appsettings 的排除配置。appsettings 本来就按环境分文件,正好各归各位。测试环境禁 ESS 用 SSO,appsettings.Test.json 写:

{
"Aegis": {
"Components": {
"Disabled": [ "EssAuthorize" ]
}
}
}

appsettings.Production.json 反过来禁 SsoAuthorize。容器环境不方便改文件时,环境变量和命令行同样有效:

Aegis__Components__Disabled__0=EssAuthorize
dotnet run -- --Aegis:Components:Disabled:0 EssAuthorize

排除是整组件语义:命中的组件服务不注册、Options 不绑定、健康探针不注册、中间件不挂载,效果等同于清单里从没写过这个名字,依赖闭包也不会把它拉回来。

排除被依赖的组件会启动失败

DependsOn 的语义是必需。清单里有 EssAuthorize(它声明依赖 Authentication、Authorization),再把 Authorization 写进 Disabled,两者不可兼得,启动直接报错并给出处置建议(见三类启动失败)。只排除没人依赖的组件(比如双认证源场景排除其一)没有问题。

这个键在装载前读取,当时配置里只有 ASP.NET Core 默认宿主源(appsettings.json、环境变量、命令行);Configs/*.json 目录和配置中心源加载得更晚,写在那里不生效也不报错。数组按索引覆盖等细节,见Startup的 Disabled 章节。

整份清单按环境切换是另一条路:appsettings 里配 ComponentDeps 指定文件名,{Environment} 运行时替换:

{
"ComponentDeps": "Component.deps.{Environment}.json"
}

指定的文件不存在时回退默认的 Component.deps.json。单组件排除用 Disabled,整份清单切换用 ComponentDeps,按需选。

启动横幅怎么读​

装配相关的横幅一共四处,排障时按这个顺序看:

  1. 组件装配解析(有自动拉入时才出现):每行「自动加载 X(被 Y 依赖)」说明 X 不在你的清单里、是依赖闭包拉进来的。拉入项不合预期,顺着 Y 的 DependsOn 找根因。
  2. 已按 Aegis:Components:Disabled 排除组件: X(服务不注册、中间件不挂载):环境排除生效的确认。
  3. 执行顺序: A → B → C:服务、中间件两个阶段各一条,是框架排定的最终序。两份环境配置是否一致,diff 这两行就知道。
  4. 检查遗漏服务 / 检查遗漏中间件:没进本阶段的组件逐个列出。「当前未加载 X」不代表故障——没写进清单的组件本来就该在这。

三类启动失败​

装配解析的问题不再拖到运行期 DI 异常,启动即报、报错带路径:

类型触发条件报错样子处置
依赖名拼错DependsOn 里的名字不在组件注册表组件 X 声明依赖 Y,但注册表中不存在该组件——DependsOn 拼写错误(声明于 Aegis.Authorization.ESS)先确认不是「包没装」:被依赖组件所在包没安装到该环境,报的也是这条。装包,或改声明
Disabled 冲突参与装配的组件依赖了被排除的组件组件 X 依赖 Y,但 Y 在 Aegis:Components:Disabled 中被排除,二者不可兼得:…按报错二选一:去掉该 Disabled 项,或从清单去掉 X。X 若是闭包自动拉入的,报错会提示根因在依赖链上的显式组件
依赖环DependsOn 边成环组件依赖存在环:A → B → C → A——请修正对应组件的 DependsOn 声明修声明。自依赖(A → A)同样算环

一条边界:清单里写了名字、但扫描没发现对应组件(比如包没装),维持既有行为——「找不到组件」Warn 加跳过,不阻断启动。拼错的是清单里的组件名给 Warn,拼错的是组件声明的 DependsOn 才会在启动时报错。

FAQ​

三个键并存会怎样?

按阶段并集,三份都算数:S = Load ∪ Services、M = Load ∪ Middlewares。旧版本三键并存时 Load 会被静默忽略(整表覆盖语义),这个历史坑 3.1.4 已修复。升级上来的项目如果三键并存,Load 里的组件会重新进入两个阶段装配,逐项确认一下预期。

清单里写了没装的组件会怎样?

「找不到组件」Warn 加跳过,不阻断启动。要留意的反而是反方向:报「DependsOn 拼写错误」时,除了名字写错,也可能是被依赖组件所在包没安装到这个环境——处置是装包,不是改声明。

想固定两个组件的先后怎么办?

deps.json 做不到这件事,数组顺序不参与行为。两个组件都是你自己写的,在依赖方(后执行的那个)的 ComponentRegister 里声明 DependsOn 指向前一个;要的是管线位置而不是依赖关系(比如必须排在认证前),覆写 Order。框架自带的组件已经配好锚点和依赖声明,直接用。

从 3.1.4 之前的版本升级,自查哪些点?

  • 书写顺序不再生效。过去靠手写顺序碰巧跑对的组件关系,重排后可能暴露为启动失败或功能异常——处置是补 DependsOn 声明,调 JSON 顺序没有用
  • 升级前后对比两阶段「执行顺序」横幅,每一处位置变化要能由 DependsOn 或 Order 锚点解释
  • 闭包会拉入新组件(典型:默认清单含 Authorization 的应用,Authentication 服务被自动拉入),在「组件装配解析」段逐项核对来源
  • 自定义 ComponentRegister 子类里若有自带的 Order 成员,改成 override,否则 TreatWarningsAsErrors 项目会炸构建(CS0108)
  • 带锚点的框架包建议同批升级,跨组半升级会让锚点保护不完整

完整清单见更新日志 v3.1.4-preview7 的升级自查段。

配套阅读​