XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

资源绑定 ​

这一页讲特性怎样拿到数据库、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 按下面的顺序收集:

  1. 环境变量 SERVICE_BINDING_ROOT 指向的目录。每个子目录是一个绑定,目录名是绑定名;目录里每个文件是一个属性,文件名是属性名、内容修剪首尾空白后是值;type 文件是类型,uri 文件既是主值也保留为属性;以 . 开头的文件跳过。AppBuilder.UseServiceBindingRoot(directory) 可以在代码里改用别的目录。
  2. 配置节 XiHan:Bindings:<name>。节本身的标量值或子键 value 是主值,子键 type 是类型,其余子键是属性。

同名时配置整体覆盖目录来源,不逐属性合并。目录布局长这样:

text
/bindings/
└─ ledger/
   ├─ type        # connection:relational
   ├─ uri         # Host=db;Database=ledger;Username=app;Password=...
   └─ password    # 可选的单独属性

同一份信息用配置节表达:

json
{
  "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 里取出主值:

csharp
// 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);
csharp
// 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 配置节里的同类键:绑定里有这一项就用绑定的,没有就读配置节的;前缀、超时、分组等不是连接与凭据的键照读配置节。主值或属性为空白时不算给出;凭据属性与主值互不依赖,绑定只给凭据、连接来自配置节也照样覆盖凭据;两者都没有连接时装配失败。属性名区分大小写,按表里的写;凭据不进日志与错误消息。

绑定名类型声明它的包用法
dbconnection:relationalPersistence.EntityFrameworkCore、Persistence.SqlSugar交给应用传入特性的配置委托,主值是连接字符串
cachecache:distributedCaching.Redis主值覆盖 XiHan:Caching:Redis:Configuration;属性 username、password 覆盖连接字符串里的 user、password
queuequeueMessaging.RabbitMQ主值覆盖 XiHan:Messaging:RabbitMQ:Configuration;属性 username、password 覆盖 AMQP URI 里的用户名与密码
streamstreamMessaging.Kafka主值覆盖 XiHan:Messaging:Kafka:BootstrapServers;属性 security.protocol、sasl.mechanism、sasl.username、sasl.password 覆盖 Properties 里的同名键(只认这四项)
nacosconfiguration:remoteConfiguration.Nacos主值覆盖 XiHan:Configuration:Nacos:ServerAddresses;属性 username、password 覆盖 Username、Password
objectstorageblobObjectStorage.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 时解析一次并交给特性装配,改了要重启。轮换凭据时用滚动重启。

相关包与下一步 ​

Released under The MIT License