XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

特性与清单 ​

这一页讲框架装配的最小单元:特性(IFeature)与它随身携带的清单(Manifest)。读完这页,你能写出一个完整的应用能力包:在清单里声明身份、依赖与贡献,在 Configure 里登记服务、权限、中间件与操作,并知道 FeatureContext 上的每个入口该在什么时候用。前置知识是装配入口。

为什么需要 ​

没有清单的模块只能靠「先引用谁、先注册谁」来表达依赖,换一个实现要改调用方,缺少一个依赖要等运行到那一行才知道。框架让每个特性自带一份结构化清单:提供哪些能力、依赖哪些能力、要哪些资源、贡献了什么。内核在构建期读清单求解依赖,缺失、成环、冲突都在启动前报出来;Configure 则只做一件事,显式登记。不扫描程序集、不靠标记接口、不靠拦截器,哪些东西进了容器,一眼能从代码里看出来。

特性 ​

特性是实现 IFeature 的类,只有两个成员:

成员作用
Manifest Manifest { get; }清单:身份、提供与依赖的能力、资源绑定、贡献点
void Configure(FeatureContext context)注册服务、中间件、权限与操作;按能力图谱的拓扑顺序调用一次

两个派生接口按需实现:

接口多出的成员用途
IHostedFeatureStartAsync(IServiceProvider, CancellationToken)、StopAsync(CancellationToken)启动时建立连接、预热,停止时释放;受启动与停止超时约束,见生命周期与健康
IShadowAwareFeatureConfigureShadowed(FeatureContext)特性声明了能力却一个都没被选中(shadowed)时,内核改调它而不是 Configure,只登记与能力选择无关的服务,见能力图谱与 Provider

特性没有基类,内核也不扫描程序集。应用用 UseFeature<T>() 显式加入每一个特性(要求公开的无参构造函数),或用 UseFeature(instance) 加入已创建的实例。能力包就是包含一个或多个特性的 NuGet 包:框架里每个子系统的默认实现与 Provider 都是能力包,应用自己的业务模块也写成能力包。

清单 ​

清单是装配的事实源,内核在构建期用 Manifest.Validate() 校验格式。清单是手写的,字段与 XiHan 规范的 manifest.schema.json 一一对应:

字段必填类型含义
Id是string清单 ID,xihan.<领域>.<名称>,应用内唯一,重复报 KERNEL_CAPABILITY_005
Package否string?所属包 ID
Version是string特性版本,SemVer
Primitives是int依赖的原语层大版本,当前为 1
Stability是StabilityStable、Preview、Experimental、Internal、Deprecated
Kind是ManifestKind见下表
Role否ManifestRoleProvider(缺省)或 Default
Provides否CapabilityDeclaration 列表提供的能力与接口版本,new CapabilityDeclaration(id, version)
Requires否CapabilityRequirement 列表依赖的能力、接口版本与是否可选,new CapabilityRequirement(id, version, optional)
Bindings否Binding 列表需要宿主注入的资源(连接串、端点),new Binding(name, type, required),见资源绑定
Contributes否Contributions贡献点,见下一节
Activation否string 列表激活事件,插件按需激活时使用
Isolation插件PluginIsolation?InProcess、Wasm、OutOfProcess,见隔离级别与清单
Permissions插件string 列表插件所需的权限
Entry否string?入口类型全名,通常写 typeof(当前特性).FullName

Kind 说明清单是什么东西,Role 说明它在同名能力的竞争里处于什么位置:

ManifestKind含义
Capability业务能力包或抽象包:声明依赖、登记操作与服务
Provider某个能力的实现,包括框架的默认实现与外部 Provider
Plugin插件
Host宿主
Protocol协议适配器
ManifestRole含义
Provider缺省;参与同名能力的竞争,加入即顶替默认实现
Default默认实现;只在没有任何显式 Provider 时被选中

清单里引用能力时用各抽象包的 <领域>Capabilities 常量,不手写字符串:KernelCapabilities.Hosting、PersistenceCapabilities.Relational、MessagingCapabilities.Queue、SecurityCapabilities.Authorization、MultiTenancyCapabilities.Context 等,接口版本是同一个类的 Version 常量。能力 ID 的完整清单见能力图谱与 Provider。

Contributes 只是声明 ​

Contributions 有五个成员,都是「告诉别人我贡献了什么」:

成员类型内容
PipelinePipelineContribution 列表中间件类型全名、阶段、Before / After 约束
Operationsstring 列表操作 ID
ConfigConfigContribution?读取的配置节与可选的 Schema
Permissionsstring 列表权限码
Healthstring 列表健康条目名

命令行宿主的 graph 命令、插件宿主与架构测试读取它,内核不据此注册任何东西:中间件、权限、操作仍要在 Configure 里经 FeatureContext 登记,两边应保持一致。Health 例外地有强约束:声明的每个健康条目都必须由同一个包实现并登记为 IHealthContributor,框架包的架构测试核对两边一致;应用不受这条测试约束,但建议同样声明。

FeatureContext ​

Configure 收到的 FeatureContext 给出本特性在图谱里的节点与全部登记入口:

成员用途
Services宿主的 IServiceCollection,显式注册服务,见依赖注入
Configuration宿主配置
Manifest、Node本特性的清单与图谱节点:Node.IsSelectedFor(能力)、Node.IsRequestedFor(能力)、Node.IsDegraded、Node.MissingOptional、Node.IsShadowed
AddMiddleware<T>(stage, placement)把中间件挂到某个阶段,归属本特性的清单 ID,并以单例注册中间件类型,见执行管道
AddPermission(PermissionDefinition)登记权限码;操作契约引用的权限码在装配完成时核对,未登记即启动失败(KERNEL_VALIDATION_005);端点引用的权限码在宿主建请求管道时核对(SECURITY_VALIDATION_004)
AddOperation<TOperation, TRequest, TResponse>(contract, requestInfo, responseInfo, schema)登记操作契约,通常由生成的 AddGeneratedOperations() 代劳,见操作契约
AddStartupCheck(Action<IServiceProvider>)登记要等容器建好才能做的装配核对,在任何特性启动之前执行,抛出即终止启动,见生命周期与健康
GetBinding(name)、TryGetBinding(name)读取宿主解析出的资源绑定,后者返回 Maybe<BindingValue>
Pipeline、Permissions、Operations、Bindings底层的管道构建器、权限表、操作登记表与绑定表

最小示例 ​

下面写一个订单能力包:一个带权限的操作、一个单例服务、两个权限码,并把消息能力声明为可选依赖。四个文件放在同一个项目里,项目引用 Profiles.Standard、Protocols.Http 与 Kernel.Generators(以分析器方式)。

操作契约(PlaceOrder.cs):

csharp
using XiHan.Framework.Kernel.Operations;
using XiHan.Framework.Primitives;

namespace Shop.Orders;

[Operation(Id, Stability = Stability.Preview, Expose = Expose.Http, Permission = "orders.write")]
public sealed class PlaceOrder : IOperation<PlaceOrderRequest, PlaceOrderResponse>
{
    public const string Id = "orders.create";

    public ValueTask<Result<PlaceOrderResponse>> HandleAsync(PlaceOrderRequest request, InvocationContext context)
        => ValueTask.FromResult(Result.Success(new PlaceOrderResponse(Guid.NewGuid(), request.Name, request.Amount)));
}

public sealed record PlaceOrderRequest(string Name, int Amount);

public sealed record PlaceOrderResponse(Guid Id, string Name, int Amount);

序列化上下文(ShopJsonContext.cs),Kernel.Generators 从这里取类型元数据:

csharp
using System.Text.Json.Serialization;

namespace Shop.Orders;

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(PlaceOrderRequest))]
[JsonSerializable(typeof(PlaceOrderResponse))]
internal sealed partial class ShopJsonContext : JsonSerializerContext;

特性(OrdersFeature.cs):

csharp
using Microsoft.Extensions.DependencyInjection;
using XiHan.Framework.Kernel;
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Kernel.Security;
using XiHan.Framework.Messaging.Abstractions;
using XiHan.Framework.Primitives;
using XiHan.Framework.Primitives.Manifests;
using XiHan.Framework.Security.Abstractions;

namespace Shop.Orders;

public sealed class OrdersFeature : IFeature
{
    public Manifest Manifest { get; } = new()
    {
        Id = "xihan.shop.orders",
        Package = "Shop.Orders",
        Version = "1.0.0",
        Primitives = 1,
        Stability = Stability.Preview,
        Kind = ManifestKind.Capability,
        Requires =
        [
            new CapabilityRequirement(KernelCapabilities.Hosting, KernelCapabilities.Version),
            // 操作带权限,要求授权能力在场;装配了 Standard 组合时由 Security.InMemory 提供
            new CapabilityRequirement(SecurityCapabilities.Authorization, SecurityCapabilities.Version),
            // 可选依赖:没有消息能力时特性进入 Degraded,应用照常启动
            new CapabilityRequirement(MessagingCapabilities.Queue, MessagingCapabilities.Version, optional: true),
        ],
        Contributes = new Contributions
        {
            Permissions = ["orders.read", "orders.write"],
            Operations = [PlaceOrder.Id],
        },
        Entry = typeof(OrdersFeature).FullName,
    };

    public void Configure(FeatureContext context)
    {
        context.Services.AddSingleton<OrderCounter>();

        // 权限码在这里登记;操作契约引用了没登记的权限码,装配完成时启动失败(KERNEL_VALIDATION_005)
        context.AddPermission(new PermissionDefinition("orders.read", "Read orders"));
        context.AddPermission(new PermissionDefinition("orders.write", "Place orders"));

        // 登记代码由 Kernel.Generators 从 [Operation] 生成,含请求与响应的 JSON Schema
        context.AddGeneratedOperations();
    }
}

public sealed class OrderCounter
{
    public int Placed { get; set; }
}

Program.cs:

csharp
using Shop.Orders;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Profiles.Standard;
using XiHan.Framework.Protocols.Http;
using XiHan.Framework.Protocols.Http.Routing;

var builder = WebApplication.CreateBuilder(args);

builder.AddXiHan(xihan => xihan
    .UseStandardProfile()
    .UseFeature<HttpProtocolFeature>()
    .UseFeature<OrdersFeature>());

var app = builder.Build();
app.UseXiHan();
app.MapXiHanHealth();
app.MapXiHanOperations();
app.Run();

运行后,启动日志的图谱里多出一个节点,三条依赖都有去向(Standard 组合包含 Messaging.InMemory,所以可选依赖也被满足):

text
xihan.shop.orders 1.0.0
  requires kernel.hosting@1 -> xihan.kernel.core
  requires security.authorization@1 -> xihan.security.inmemory
  requires messaging.queue@1 -> xihan.messaging.inmemory

不带凭据调用 POST /orders,Authorize 阶段按契约的 Permission 判定,返回 401、错误码 SECURITY_DENIED_001;在 XiHan:Security:InMemory 下配置一个持有 orders.write 的主体后,带 Authorization: ApiKey <值> 再调用得到 201 与订单 JSON。配置写法见授权与权限。

进阶 ​

一个真实能力包还登记什么 ​

仓库 framework/samples/SimpleApp 的 OrdersFeature 比上面多三件事,都是在 Configure 里一行一个地显式登记:

csharp
// 仓储登记由 Persistence.Generators 从实体类生成(Order : IAggregateRoot<Guid>)
context.Services.AddGeneratedRepositories();
// 聚合记录的 OrderPlaced 在工作单元提交前发到 orders 主题;装配了发件箱时与订单写入同事务
context.Services.AddDomainEvent<OrderPlaced>(Topic, "orders.placed", SampleJsonContext.Default.OrderPlaced, static placed => $"order/{placed.OrderId:N}");
context.Services.AddMessageHandler<OrderPlacedHandler>(Topic);

它的清单因此多依赖 persistence.relational 与 multitenancy.context 两个能力。仓储、领域事件与消息处理器的写法分别见仓储读写、领域事件与发件箱与发布与订阅。

只为被选中的能力登记 ​

一个特性可以同时提供几个能力,例如 Caching.InMemory 提供 caching.local 与 caching.distributed。加入 Redis Provider 后它只负责前者,这时它在 Configure 里用 context.Node.IsSelectedFor(CachingCapabilities.Distributed) 判断要不要注册分布式缓存的服务。自己写 Provider 时照这个做法,见写一个 Provider。

需要启动与停止逻辑 ​

把 IFeature 换成 IHostedFeature,在 StartAsync 里从根容器取服务建立连接,在 StopAsync 里释放。每个 StartAsync 受 XiHan:Kernel:StartTimeoutSeconds 约束,超时或抛出按失败策略处理。完整示例与状态机见生命周期与健康。

需要外部资源 ​

数据库连接串、对象存储端点这类值不写进代码,清单的 Bindings 声明名字与类型,宿主从配置节 XiHan:Bindings 或 Service Binding 目录解析,Configure 里用 context.GetBinding(name).Value 读取;必需绑定缺失时 Build() 报 KERNEL_HOSTING_001。见资源绑定。

常见问题 ​

特性一定要有无参构造函数吗 ​

UseFeature<T>() 需要,因为内核要自己创建实例。需要构造参数(比如按环境传选项)时,自己创建后用 UseFeature(instance) 加入。注意特性实例在容器建好之前就存在,构造函数里不能依赖容器服务;运行期依赖在 IHostedFeature.StartAsync 的 IServiceProvider 里取。

清单 ID 怎么取 ​

文法是 xihan.<领域>.<名称>,全小写。应用的业务包用自己的领域词,如 xihan.shop.orders;框架包按子系统与机制命名,如 xihan.caching.redis。ID 在一个应用里唯一,也是日志、图谱与管道归属里识别这个特性的名字。

Contributes 不写会怎样 ​

装配不受影响,Configure 里登记的东西照常生效。少的是可见性:graph 命令看不到你的权限与操作,插件宿主与架构测试也无法核对。建议声明,保持与 Configure 一致。

一个包可以有多个特性吗 ​

可以。每个特性有自己的清单与 Configure,各自 UseFeature 加入。把可独立选用的部分拆成单独的特性(例如一个子系统的「核心」与「可选的预签名地址」),应用只装配需要的那几个。

我能用反射扫描程序集自动 UseFeature 吗 ​

框架不提供,也不建议。显式列出特性是装配可读、可裁剪、可 Native AOT 发布的前提。要少写几行就用组合包,或者自己写一个扩展方法把一组 UseFeature 封起来。

相关包与下一步 ​

Released under The MIT License