能力图谱与 Provider
这一页讲内核怎样把一堆清单变成一张图:能力 ID 与接口版本的文法、求解的六个步骤、可选依赖缺失时的 Degraded、同一能力有多个候选时怎样选出 Provider,以及求解失败时的五个错误码。读完这页,你能看懂 graph 命令的每一行,知道加入一个 Provider 包为什么会自动顶替默认实现,也知道两个 Provider 冲突时该在哪里指定。前置知识是特性与清单。
为什么需要
应用里的依赖如果按包名写死,换一个实现就要改所有引用方;如果完全靠容器的「后注册覆盖先注册」,谁最终生效取决于注册顺序,不可读也不可验证。框架让依赖按能力 ID 声明:业务包只说「我要 persistence.relational@1」,不关心谁提供;内核在构建期为每条依赖选出唯一的提供者,默认实现自动让位给显式 Provider,有歧义就在启动前报错并列出候选。结果是一张可打印的图,部署前就能核对。
能力 ID 与接口版本
能力 ID 的文法是 <领域>.<能力>:领域是术语表领域词的小写形态,能力用小写字母、数字与连字符。引用时带整数接口版本,写作 persistence.relational@1;求解时接口版本必须相等。接口变更只增加新版本并存(@2),不改旧版本的语义,因此按 @1 写的 Provider 在当前大版本内始终可用。
| 能力 ID | 含义 | 默认实现 |
|---|---|---|
kernel.hosting、kernel.capabilities、kernel.pipeline、kernel.diagnostics、kernel.security、kernel.plugins | 宿主、能力图谱、管道、诊断、安全原语、插件抽象 | 内核自身(清单 xihan.kernel.core) |
persistence.relational | 仓储与工作单元 | Persistence.InMemory |
caching.local、caching.distributed | 进程内缓存、共享缓存与分布式锁 | Caching.InMemory |
messaging.queue、messaging.stream | 队列、有序流与重放 | Messaging.InMemory(只提供队列) |
security.authentication、security.authorization | 认证、授权 | Security.InMemory |
multitenancy.context、multitenancy.sharding | 租户上下文、租户分片 | MultiTenancy.InMemory |
localization.resources | 本地化资源 | Localization.InMemory |
objectstorage.blob | 对象存储 | ObjectStorage.FileSystem |
scheduling.jobs、scheduling.leader | 作业调度、领导选举 | Scheduling.InMemory |
auditing.trail | 审计写入与查询 | Auditing.InMemory |
workflow.execution | 工作流 | Workflow.InMemory |
ai.chat、ai.embedding、ai.tools、ai.knowledge | AI 端口 | ai.tools 由 AIToolsFeature 提供 |
protocols.http 等 protocols.* | 协议适配器 | 对应的 Protocols.* 包 |
每个子系统的抽象包都有一个 <领域>Capabilities 静态类,给出能力 ID 常量与 Version 常量。内核的那一个是 KernelCapabilities:Hosting、Capabilities、Pipeline、Diagnostics、Security、Plugins 六个常量加 Version,还有静态属性 Manifest,就是内核自己的清单。清单里只引用常量,不手写字符串。全部已登记的能力与各 Provider 的清单 ID 见包参考。
求解过程
AddXiHan 的回调返回后,AppBuilder.Build() 依次执行:
- 收集清单:内核清单
xihan.kernel.core(提供全部kernel.*能力)加上所有经UseFeature加入的特性,校验每份清单的格式与 ID 唯一。 - 为每条
Requires选出唯一的提供者(规则见「选择 Provider」);没有被任何特性依赖的能力也按同一规则选出提供者。 - 把选择结果回填到图谱节点:每个节点知道自己被选中提供哪些能力(
ResolvedNode.SelectedCapabilities)。声明了能力却一个都没被选中的节点标为 shadowed,不装配、不启动;实现了IShadowAwareFeature的特性改调ConfigureShadowed。节点还区分每项选中是不是被要求的(IsRequestedFor):有特性依赖这项能力,或XiHan:Providers/UseProvider指定了提供者,是被要求的;没人依赖也没人指定、只在第 2 步兜底选出的不是。兜底选出照样算选中,图谱不变,特性可据此只为被要求的能力承担跨能力的约束。 - 拓扑排序:被依赖者在前,
UseFeature的顺序只决定同一层次内的次序。 - 解析资源绑定,校验每个特性的必需绑定(缺失报
KERNEL_HOSTING_001)。 - 按拓扑顺序调用各特性的
Configure,再把App、内置中间件、权限表、操作登记表与宿主服务注册进容器。
可选依赖与 Degraded
CapabilityRequirement 的第三个参数 optional 为 true 时,这条依赖缺失不阻止启动:特性照常 Configure 与启动,但状态是 Degraded 而不是 Ready,图谱里这条依赖的去向打印为 (missing, optional),健康端点的 features 里也能看到。特性在 Configure 里用 context.Node.IsDegraded 与 context.Node.MissingOptional 判断缺了什么,据此跳过相关注册。可选依赖适合「有则增强、无则照常」的关系,比如有消息能力就发事件,没有就只写库。
最小示例
下面用 Minimal 组合装配一个把消息能力声明为可选依赖的特性,观察图谱与健康端点怎样报告 Degraded。
using XiHan.Framework.Kernel;
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Messaging.Abstractions;
using XiHan.Framework.Primitives;
using XiHan.Framework.Primitives.Manifests;
namespace Shop.Notifications;
public sealed class NotificationsFeature : IFeature
{
public Manifest Manifest { get; } = new()
{
Id = "xihan.shop.notifications",
Version = "1.0.0",
Primitives = 1,
Stability = Stability.Preview,
Kind = ManifestKind.Capability,
Requires =
[
new CapabilityRequirement(KernelCapabilities.Hosting, KernelCapabilities.Version),
new CapabilityRequirement(MessagingCapabilities.Queue, MessagingCapabilities.Version, optional: true),
],
Entry = typeof(NotificationsFeature).FullName,
};
public void Configure(FeatureContext context)
{
// 缺少可选依赖时只登记不依赖消息的部分;有消息能力时再登记订阅
if (context.Node.IsDegraded)
{
return;
}
}
}using Shop.Notifications;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Profiles.Minimal;
var builder = WebApplication.CreateBuilder(args);
// Minimal 组合没有消息子系统,messaging.queue@1 无人提供
builder.AddXiHan(xihan => xihan
.UseMinimalProfile()
.UseFeature<NotificationsFeature>());
var app = builder.Build();
app.UseXiHan();
app.MapXiHanHealth();
app.Run();启动日志里这个节点的依赖去向:
xihan.shop.notifications 1.0.0
requires kernel.hosting@1 -> xihan.kernel.core
requires messaging.queue@1 -> (missing, optional)curl -s localhost:5000/health/ready 返回 200,应用仍在服务,但状态是 Degraded:
{
"status": "pass",
"state": "Degraded",
"features": [
{ "id": "xihan.localization.inmemory", "state": "Ready" },
{ "id": "xihan.shop.notifications", "state": "Degraded" }
],
"contributions": []
}把 UseMinimalProfile() 换成 UseStandardProfile(),Messaging.InMemory 进入图谱,依赖去向变成 -> xihan.messaging.inmemory,特性与应用都回到 Ready。
选择 Provider
同一能力有多个候选时,按以下顺序选出一个:
- 显式指定:代码里的
AppBuilder.UseProvider(capabilityId, manifestId),其次配置XiHan:Providers:<能力 ID>;两者都有时代码优先。指定的清单不存在或接口版本不符时,必需依赖直接失败。 - 唯一的
Role = Provider候选。 - 唯一的
Role = Default候选。默认实现只在没有任何显式 Provider 时参与竞争。 - 以上都不成立:报
KERNEL_CAPABILITY_004并列出候选。
因此加入一个 Provider 就会顶替同一能力的默认实现,不需要移除组合包里的 UseFeature:
using XiHan.Framework.Caching.Redis;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Profiles.Standard;
using XiHan.Framework.Protocols.Http;
var builder = WebApplication.CreateBuilder(args);
builder.AddXiHan(xihan => xihan
.UseStandardProfile() // 含 Caching.InMemory(default),提供 caching.local 与 caching.distributed
.UseFeature<RedisCachingFeature>() // Provider:caching.distributed 改由 Redis 提供
.UseFeature<HttpProtocolFeature>());默认实现若还提供别的、未被顶替的能力,仍然装配,但只为被选中的能力注册服务。图谱里相应地打出两种标记:
xihan.caching.inmemory <版本> (default)
provides caching.local@1
provides caching.distributed@1 (not selected)
requires kernel.hosting@1 -> xihan.kernel.core
xihan.caching.redis <版本>
provides caching.distributed@1
requires kernel.hosting@1 -> xihan.kernel.core两个显式 Provider 提供同一能力时必须指定,否则 Build() 报 KERNEL_CAPABILITY_004。配置与代码两种写法等价,代码优先:
{
"XiHan": {
"Providers": {
"caching.distributed": "xihan.caching.redis"
}
}
}builder.AddXiHan(xihan => xihan
.UseStandardProfile()
.UseFeature<RedisCachingFeature>() // 提供 caching.distributed@1
.UseFeature<RemoteCachingFeature>() // 同样提供 caching.distributed@1
.UseProvider("caching.distributed", "xihan.caching.redis"));没有被选中的 xihan.caching.remote 一个能力都不提供,在图谱里标为 (shadowed),不装配、不启动。各 Provider 的清单 ID 写在对应包页的信息表里。
冲突与错误码
求解失败时 Build() 抛出 FrameworkException,消息列出冲突双方的清单 ID:
| 错误码 | 情形 | 怎么办 |
|---|---|---|
KERNEL_CAPABILITY_001 | 必需依赖没有任何清单提供 | 加入提供该能力的默认实现或 Provider,或把依赖改为可选 |
KERNEL_CAPABILITY_002 | 依赖成环 | 把共享部分抽到第三个特性,让两边都依赖它 |
KERNEL_CAPABILITY_003 | 有清单提供同名能力,但接口版本不符 | 升级 Provider 或改依赖的版本号 |
KERNEL_CAPABILITY_004 | 同一能力有多个候选且没有指定 | 用 UseProvider 或 XiHan:Providers 指定 |
KERNEL_CAPABILITY_005 | 清单 ID 重复 | 同一特性加入了两次,或两个特性用了同一个 ID |
同一阶段还可能遇到 KERNEL_HOSTING_001(必需的资源绑定缺失)与 KERNEL_VALIDATION_005(操作契约引用了未登记的权限码),它们不是求解错误,但同样在 Build() 里抛出。
查看图谱
| 方式 | 输出 |
|---|---|
| 启动日志(Information 级别) | Capability graph: 后的全部节点 |
命令行宿主 graph 命令 | 与启动日志相同 |
App.DescribeGraph() | 同上,返回字符串 |
App.Graph | ResolvedGraph 对象:Nodes(拓扑序)、Providers(能力 ID 到提供者节点)、this[manifestId]、Contains(manifestId)、Describe() |
ResolvedNode 是图谱里的一个节点,特性在 Configure 里经 context.Node 拿到自己的那一个:
| 成员 | 含义 |
|---|---|
Manifest | 节点的清单 |
Dependencies | 被选中的依赖节点 |
SelectedCapabilities | 本节点被选中提供的能力 ID |
IsSelectedFor(capabilityId) | 是否被选中提供某能力 |
IsRequestedFor(capabilityId) | 这项选中是不是被要求的(有人依赖或有人指定) |
IsShadowed | 声明了能力却一个都没被选中 |
IsDegraded、MissingOptional | 是否缺少可选依赖,以及缺了哪些 |
命令行宿主上直接运行:
dotnet run --project framework/samples/SimpleCli -- graph常见问题
我只想换掉一个子系统的实现,要动组合包吗
不用。保留 UseStandardProfile(),再 UseFeature<某个 Provider 特性>() 即可:Provider 的 Role 是 Provider,默认实现的 Role 是 Default,前者自动胜出。这也是组合包能保持简单的原因。
两个 Provider 都想保留,分别服务不同用途怎么办
能力图谱对一个能力只选一个提供者。需要同时用两个(比如两个对象存储桶、两个缓存实例)时,看该子系统是否支持命名实例,例如对象存储的命名实例 Provider 见分片、预签名与命名实例;没有命名实例机制的子系统,第二个实例作为应用自己的服务注册,不走能力 ID。
为什么 Provider 加进来了,它的中间件却没出现在管道里
先看图谱:节点若标 (shadowed),说明它提供的能力都被别的节点拿走了,它根本没有 Configure。再看 (not selected):它可能只为被选中的那部分能力登记了中间件。最后看是否被 XiHan:Providers 指向了别的清单。
接口版本 @1 和包版本是一回事吗
不是。@1 是能力接口的版本,只在契约不兼容地变化时递增;包版本按发布节奏递增,与接口版本无关。所有框架包共享同一条包版本线,而大多数能力长期停在 @1。
应用自己的业务包要不要 Provides
通常不要。业务包是能力的消费者,只写 Requires。只有当你希望别的包按能力 ID 依赖你(比如公司内部的「客户主数据」能力有多种实现)时才声明 Provides,并为它定义一个 <领域>Capabilities 常量类。
相关包与下一步
- 包参考:Kernel、Primitives、包总览(全部能力 ID 与各 Provider 的清单 ID)
- 下一页:生命周期与健康,看求解出的图谱怎样按顺序启动、停止与上报健康
- 延伸:写一个 Provider、组合包与选包
