XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

能力图谱与 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.knowledgeAI 端口ai.tools 由 AIToolsFeature 提供
protocols.http 等 protocols.*协议适配器对应的 Protocols.* 包

每个子系统的抽象包都有一个 <领域>Capabilities 静态类,给出能力 ID 常量与 Version 常量。内核的那一个是 KernelCapabilities:Hosting、Capabilities、Pipeline、Diagnostics、Security、Plugins 六个常量加 Version,还有静态属性 Manifest,就是内核自己的清单。清单里只引用常量,不手写字符串。全部已登记的能力与各 Provider 的清单 ID 见包参考。

求解过程 ​

AddXiHan 的回调返回后,AppBuilder.Build() 依次执行:

  1. 收集清单:内核清单 xihan.kernel.core(提供全部 kernel.* 能力)加上所有经 UseFeature 加入的特性,校验每份清单的格式与 ID 唯一。
  2. 为每条 Requires 选出唯一的提供者(规则见「选择 Provider」);没有被任何特性依赖的能力也按同一规则选出提供者。
  3. 把选择结果回填到图谱节点:每个节点知道自己被选中提供哪些能力(ResolvedNode.SelectedCapabilities)。声明了能力却一个都没被选中的节点标为 shadowed,不装配、不启动;实现了 IShadowAwareFeature 的特性改调 ConfigureShadowed。节点还区分每项选中是不是被要求的(IsRequestedFor):有特性依赖这项能力,或 XiHan:Providers / UseProvider 指定了提供者,是被要求的;没人依赖也没人指定、只在第 2 步兜底选出的不是。兜底选出照样算选中,图谱不变,特性可据此只为被要求的能力承担跨能力的约束。
  4. 拓扑排序:被依赖者在前,UseFeature 的顺序只决定同一层次内的次序。
  5. 解析资源绑定,校验每个特性的必需绑定(缺失报 KERNEL_HOSTING_001)。
  6. 按拓扑顺序调用各特性的 Configure,再把 App、内置中间件、权限表、操作登记表与宿主服务注册进容器。

可选依赖与 Degraded ​

CapabilityRequirement 的第三个参数 optional 为 true 时,这条依赖缺失不阻止启动:特性照常 Configure 与启动,但状态是 Degraded 而不是 Ready,图谱里这条依赖的去向打印为 (missing, optional),健康端点的 features 里也能看到。特性在 Configure 里用 context.Node.IsDegraded 与 context.Node.MissingOptional 判断缺了什么,据此跳过相关注册。可选依赖适合「有则增强、无则照常」的关系,比如有消息能力就发事件,没有就只写库。

最小示例 ​

下面用 Minimal 组合装配一个把消息能力声明为可选依赖的特性,观察图谱与健康端点怎样报告 Degraded。

csharp
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;
        }
    }
}
csharp
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();

启动日志里这个节点的依赖去向:

text
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:

json
{
  "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 ​

同一能力有多个候选时,按以下顺序选出一个:

  1. 显式指定:代码里的 AppBuilder.UseProvider(capabilityId, manifestId),其次配置 XiHan:Providers:<能力 ID>;两者都有时代码优先。指定的清单不存在或接口版本不符时,必需依赖直接失败。
  2. 唯一的 Role = Provider 候选。
  3. 唯一的 Role = Default 候选。默认实现只在没有任何显式 Provider 时参与竞争。
  4. 以上都不成立:报 KERNEL_CAPABILITY_004 并列出候选。

因此加入一个 Provider 就会顶替同一能力的默认实现,不需要移除组合包里的 UseFeature:

csharp
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>());

默认实现若还提供别的、未被顶替的能力,仍然装配,但只为被选中的能力注册服务。图谱里相应地打出两种标记:

text
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

Redis 的连接经资源绑定给出,见缓存读写与资源绑定。

两个显式 Provider 提供同一能力时必须指定,否则 Build() 报 KERNEL_CAPABILITY_004。配置与代码两种写法等价,代码优先:

json
{
  "XiHan": {
    "Providers": {
      "caching.distributed": "xihan.caching.redis"
    }
  }
}
csharp
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.GraphResolvedGraph 对象:Nodes(拓扑序)、Providers(能力 ID 到提供者节点)、this[manifestId]、Contains(manifestId)、Describe()

ResolvedNode 是图谱里的一个节点,特性在 Configure 里经 context.Node 拿到自己的那一个:

成员含义
Manifest节点的清单
Dependencies被选中的依赖节点
SelectedCapabilities本节点被选中提供的能力 ID
IsSelectedFor(capabilityId)是否被选中提供某能力
IsRequestedFor(capabilityId)这项选中是不是被要求的(有人依赖或有人指定)
IsShadowed声明了能力却一个都没被选中
IsDegraded、MissingOptional是否缺少可选依赖,以及缺了哪些

命令行宿主上直接运行:

bash
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 常量类。

相关包与下一步 ​

Released under The MIT License