资源绑定
这一页讲特性怎样拿到数据库、Redis、消息代理这类外部资源的连接信息:在清单里声明一个绑定,由宿主在装配时从 Service Binding 目录或配置节解析出来,特性用名字取用。读完你能给自己的特性声明一个必需绑定,并知道缺了它应用为什么在启动期就失败。前置知识是 配置模型与选项与特性与清单。
为什么需要
同一个能力包要在开发机、Compose、Kubernetes 里运行,连接信息却各不相同:本地是 localhost:6379,集群里是某个 Service 的地址加一份 Secret 里的口令。如果特性直接读某个固定的配置键,每换一个环境就要改配置的形状;如果连接信息缺失,往往要等到第一次真正连接时才发现。
框架把「我需要一个什么类型的资源」与「这个资源在哪」分开:特性在清单的 Bindings 里按名字声明需要的资源与类型,宿主在 AddXiHan 时从环境里解析出 BindingValue,缺失的必需绑定立刻让启动失败。部署工具 xihan pack 按同样的名字生成 Secret 与环境变量,代码一行不改。
概念
| 概念 | 类型 | 说明 |
|---|---|---|
| 绑定声明 | Binding(name, type, required = true) | 写在清单的 Bindings 里;type 是资源类型,如 connection:relational、cache:distributed |
| 绑定值 | BindingValue(Name, Type, Value, Properties) | 宿主解析出的结果:名字、类型、主值(连接字符串或地址)与其余属性;Property(key) 取单个属性 |
| 解析器 | BindingResolver.Resolve(configuration, serviceBindingRoot) | 内核按固定顺序收集绑定 |
| 取用 | FeatureContext.GetBinding(name) / TryGetBinding(name) | 在 Configure 里取;前者缺失时抛出,后者返回 Maybe<BindingValue> |
| 来源一 | SERVICE_BINDING_ROOT 目录 | Service Binding 规范的目录布局,Kubernetes 里由 Secret 挂载 |
| 来源二 | 配置节 XiHan:Bindings:<name> | 本地开发、Compose 与 Aspire 用环境变量注入 |
绑定的来源与优先级
BindingResolver.Resolve 按下面的顺序收集:
- 环境变量
SERVICE_BINDING_ROOT指向的目录。每个子目录是一个绑定,目录名是绑定名;目录里每个文件是一个属性,文件名是属性名、内容修剪首尾空白后是值;type文件是类型,uri文件既是主值也保留为属性;以.开头的文件跳过。AppBuilder.UseServiceBindingRoot(directory)可以在代码里改用别的目录。 - 配置节
XiHan:Bindings:<name>。节本身的标量值或子键value是主值,子键type是类型,其余子键是属性。
同名时配置整体覆盖目录来源,不逐属性合并。目录布局长这样:
/bindings/
└─ ledger/
├─ type # connection:relational
├─ uri # Host=db;Database=ledger;Username=app;Password=...
└─ password # 可选的单独属性同一份信息用配置节表达:
{
"XiHan": {
"Bindings": {
"cache": "localhost:6379",
"ledger": {
"type": "connection:relational",
"value": "Host=localhost;Database=ledger;Username=dev;Password=dev-password"
}
}
}
}环境变量写法是 XiHan__Bindings__ledger__value、XiHan__Bindings__ledger__type,Compose 与 Aspire 的生成产物就是这样注入的。
最小示例
我们写一个需要关系数据库连接的记账特性,声明一个必需绑定 ledger,并在 Configure 里取出主值:
// BillingFeature.cs
using Microsoft.Extensions.DependencyInjection;
using XiHan.Framework.Kernel;
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Primitives;
using XiHan.Framework.Primitives.Manifests;
namespace Billing;
public sealed class BillingFeature : IFeature
{
public Manifest Manifest { get; } = new()
{
Id = "shop.billing.ledger",
Package = "Billing",
Version = "1.0.0",
Primitives = 1,
Stability = Stability.Experimental,
Kind = ManifestKind.Capability,
Requires = [new CapabilityRequirement(KernelCapabilities.Hosting, KernelCapabilities.Version)],
// 必需绑定:缺失时 AddXiHan 抛出 KERNEL_HOSTING_001,启动前就暴露
Bindings = [new Binding("ledger", "connection:relational")],
Entry = typeof(BillingFeature).FullName,
};
public void Configure(FeatureContext context)
{
var ledger = context.GetBinding("ledger");
// 必需绑定一定存在,但主值可能为空:空值是部署错误,抛出让启动失败
var connectionString = ledger.Value ?? throw new InvalidOperationException("Binding 'ledger' has no value.");
context.Services.AddSingleton(new LedgerConnection(connectionString));
}
}
public sealed record LedgerConnection(string ConnectionString);// Program.cs
using Billing;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Profiles.Standard;
var builder = WebApplication.CreateBuilder(args);
builder.AddXiHan(xihan => xihan.UseStandardProfile().UseFeature<BillingFeature>());
var app = builder.Build();
app.UseXiHan();
app.MapGet("/ledger", (LedgerConnection ledger) => ledger.ConnectionString.Length);
app.Run();先不给任何绑定直接 dotnet run:AddXiHan 抛出错误码为 KERNEL_HOSTING_001 的 FrameworkException,消息指出缺少绑定 ledger,进程退出。再把上一节的 XiHan:Bindings:ledger 写进 appsettings.json,或设置环境变量 XiHan__Bindings__ledger__value=... 与 XiHan__Bindings__ledger__type=connection:relational,应用正常启动,curl localhost:5000/ledger 返回连接字符串的长度。换成 Kubernetes 时把同样的内容做成 Secret 挂到 SERVICE_BINDING_ROOT,代码不变。
必需绑定与启动期失败
清单里 required: true(缺省)的绑定在 AddXiHan 构建时校验:
| 情形 | 结果 |
|---|---|
| 必需绑定缺失 | 抛出 KERNEL_HOSTING_001,应用不启动 |
| 来源声明了类型且与清单不一致 | 同上 |
| 来源没有声明类型(空字符串) | 不比对类型,按名字匹配 |
可选绑定(required: false)缺失 | 不报错,特性用 TryGetBinding 自行处理 |
GetBinding 取一个不存在的绑定 | 抛出 KERNEL_HOSTING_001 |
这正是「配置与编程错误抛异常、可恢复失败返回 Result」的分界:缺绑定是部署错误,不应该带着它运行,见结果与错误处理。
内置包声明的绑定
框架内置包声明的绑定都是可选的。绑定来自部署环境,它给出的连接信息(主值)与凭据(属性)逐项覆盖 Provider 配置节里的同类键:绑定里有这一项就用绑定的,没有就读配置节的;前缀、超时、分组等不是连接与凭据的键照读配置节。主值或属性为空白时不算给出;凭据属性与主值互不依赖,绑定只给凭据、连接来自配置节也照样覆盖凭据;两者都没有连接时装配失败。属性名区分大小写,按表里的写;凭据不进日志与错误消息。
| 绑定名 | 类型 | 声明它的包 | 用法 |
|---|---|---|---|
db | connection:relational | Persistence.EntityFrameworkCore、Persistence.SqlSugar | 交给应用传入特性的配置委托,主值是连接字符串 |
cache | cache:distributed | Caching.Redis | 主值覆盖 XiHan:Caching:Redis:Configuration;属性 username、password 覆盖连接字符串里的 user、password |
queue | queue | Messaging.RabbitMQ | 主值覆盖 XiHan:Messaging:RabbitMQ:Configuration;属性 username、password 覆盖 AMQP URI 里的用户名与密码 |
stream | stream | Messaging.Kafka | 主值覆盖 XiHan:Messaging:Kafka:BootstrapServers;属性 security.protocol、sasl.mechanism、sasl.username、sasl.password 覆盖 Properties 里的同名键(只认这四项) |
nacos | configuration:remote | Configuration.Nacos | 主值覆盖 XiHan:Configuration:Nacos:ServerAddresses;属性 username、password 覆盖 Username、Password |
objectstorage | blob | ObjectStorage.S3 | 主值(服务端点)与属性 region、accessKey、secretKey、forcePathStyle 覆盖 XiHan:ObjectStorage:S3 里的同名设置 |
与部署产物的关系
表里六种绑定类型 xihan pack 都认识:应用清单 xihan.app.json 按上表的绑定名声明后,Compose 与 Aspire 产物会起对应的依赖服务并把主值注入 XiHan__Bindings__<绑定名>__value(blob 的依赖服务是 S3 兼容的 RustFS,另注入 accessKey、secretKey 两个属性,其余服务的凭据在主值里或没开认证);Kustomize 产物不在集群里起依赖服务,只按 Service Binding 规范挂 Secret,示例 Secret 以注释列出各类型可单独给出的凭据键。生成规则见应用清单与 xihan pack。
常见问题
绑定与配置节有什么区别,什么时候用哪个
配置节放行为参数(超时、前缀、容量),绑定放「连到哪、用什么凭据」。绑定由部署环境注入,Provider 用它覆盖配置节里的连接与凭据键,其余键照读配置节。本地开发没有 Service Binding 目录时,用 XiHan:Bindings 配置节写同样的内容。
必需绑定在生产里一定要用 Service Binding 目录吗
不一定。目录与配置节两种来源等价,Kustomize 产物用目录,Compose 与 Aspire 用环境变量注入配置节;只要 BindingResolver 能解析到,特性拿到的 BindingValue 相同。
同一个绑定在目录与配置节里都有,取哪个
配置节整体覆盖目录来源,连属性也一起换掉,不逐项合并。
绑定的主值会出现在日志里吗
不会。BindingValue.ToString() 把 Value 打印成 ***,Provider 的选项记录也遮蔽连接字符串与口令,见 远程配置与密钥的「选项打印时遮蔽凭据」。
绑定的值能热更新吗
不能。绑定在 AddXiHan 时解析一次并交给特性装配,改了要重启。轮换凭据时用滚动重启。
相关包与下一步
- Kernel:
BindingResolver、BindingValue、FeatureContext.GetBinding - Primitives:清单模型
Manifest与Binding - 应用清单与 xihan pack:按绑定名生成 Secret 与环境变量
- 下一页:远程配置与密钥
