XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

XiHan.Framework.Configuration.Abstractions ​

配置子系统的抽象包:定义远程配置客户端 IRemoteConfigurationClient 与密钥库 ISecretStore 两个 Provider 契约,以及把它们接进 IConfigurationBuilder 的配置源。应用仍然读 IConfiguration,需要从配置中心或密钥库取值时引用本包。

项内容
包 IDXiHan.Framework.Configuration.Abstractions
环层Ring 2(抽象)
能力定义 configuration.remote@1 与 configuration.secrets@1,本包不提供实现
特性入口—
配置节—(Provider 的配置节在各自包里,如 XiHan:Configuration:Nacos)
依赖XiHan.Framework.Primitives、XiHan.Framework.Localization.Abstractions;Microsoft.Extensions.Configuration.Abstractions
AOT兼容
稳定性Preview

概述 ​

框架不另造配置模型,默认的配置来源就是 .NET 原生的文件、环境变量与命令行,所以配置子系统没有默认实现包。本包只补两类 .NET 没有统一契约的来源:

  • 远程配置(configuration.remote@1):按命名空间取整份带版本的文档,键已展平成 A:B:C。AddXiHanRemoteConfiguration 构建配置时同步加载一次,之后按周期带已知版本轮询,版本变化时整体替换并触发重载令牌,失败保留最后一份成功的数据。它有两个重载:传入现成的客户端(由调用方持有),或传入工厂(客户端归配置源,随配置根释放)。
  • 密钥(configuration.secrets@1):按名字批量读取,只读,逐项返回结果;SecretValue.ToString() 固定返回遮蔽文本。AddXiHanSecrets(store, options) 把密钥映射到配置键,构建时一次批量读取,可周期重读。

两个配置源都派生自内部的键值表 Provider:数据整体替换,替换时先换上新令牌再取消旧令牌;子键的枚举顺序与 Microsoft.Extensions.Configuration 一致(数字段按数值、其余按忽略大小写的序数)。键不区分大小写。

何时使用 ​

  • 用 Nacos 做配置中心:装 Configuration.Nacos,它实现 IRemoteConfigurationClient,本包随之引入。
  • 接入其他配置中心或密钥库(Apollo、Consul、Vault、Key Vault 等):框架没有提供这些 Provider,应用实现本包的接口,并用一致性套件验证。
  • 只用文件与环境变量:不需要本包,见配置与选项。

安装与启用 ​

bash
dotnet add package XiHan.Framework.Configuration.Abstractions --prerelease

当前是预览版,安装要带 --prerelease;从源码或本地包源引用见快速上手。Profiles.Full 引用了本包。

本包没有特性。配置源加在 builder.Configuration 上,并且要在 AddXiHan 之前,框架在 AddXiHan 里读取各包选项与绑定。应用自己实现的 Provider 要让错误按调用方语言翻译时,注册本包的消息目录:

csharp
builder.Services.AddMessageCatalog(ConfigurationMessages.Catalog);

配置 ​

本包没有配置节,两个配置源的选项在代码里构造。

RemoteConfigurationOptions:

属性类型缺省说明
Namespacestring构造参数,不能为空白远程命名空间
Prefixstring?null把文档挂到的配置节,为空时挂在根
PollIntervalTimeSpan?30 秒轮询周期,null 关闭,必须为正
ClockTimeProvider系统时钟轮询计时用的时钟,不能为空;测试换成可推进的时钟
LoadTimeoutTimeSpan10 秒首次加载的超时,必须为正
Optionalboolfalse首次加载失败时继续(该源为空,照常轮询)还是抛出
OnErrorAction<Error>?null加载或轮询失败的回调,配置在日志之前构建,这是唯一的报告渠道;回调抛出的异常被丢弃

SecretsConfigurationOptions:

属性类型缺省说明
MappingsIReadOnlyDictionary<string, SecretName>构造参数,至少一项配置键 → 密钥名
LoadTimeoutTimeSpan10 秒首次读取的超时
RefreshIntervalTimeSpan?null周期重读的间隔,null 不重读
ClockTimeProvider系统时钟重读计时用的时钟,不能为空;测试换成可推进的时钟
Optionalboolfalse首次读取缺失或失败的项跳过还是抛出;整体失败时该源为空,照常重读
OnErrorAction<Error>?null每个失败的项回调一次,整体失败回调一次;回调抛出的异常被丢弃

主要类型 ​

类型说明
Remote.IRemoteConfigurationClientFetchAsync(namespaceName, knownVersion, cancellationToken) → Result<Maybe<ConfigurationDocument>>;knownVersion 为空总是返回文档,非空且未变化返回无值,可长轮询,收到取消返回无值
Remote.ConfigurationDocument版本(ETag、修订号或内容哈希,非空)与展平的键值;Empty(version) 建空文档
Remote.RemoteConfigurationOptions远程配置源的选项
Remote.RemoteConfigurationBuilderExtensionsAddXiHanRemoteConfiguration(client, options):客户端由调用方持有,配置根释放时不释放它;AddXiHanRemoteConfiguration(clientFactory, options):每次构建 Provider 按工厂新建一个客户端,随 Provider 释放
Remote.RemoteConfigurationSource传入现成客户端时对应的 IConfigurationSource
Secrets.ISecretStoreGetAsync(names, cancellationToken) 批量逐项返回 SecretLookup,顺序与入参一致
Secrets.SecretName密钥名,^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$,不含 ..、//,不以 / 结尾,大小写敏感;可从 string 隐式转换
Secrets.SecretValue值与可选版本;ToString() 返回 Redacted(***)
Secrets.SecretLookup一项结果:Name 与 Result<SecretValue>
Secrets.SecretStoreExtensions单条读取 GetAsync(store, name, cancellationToken),转调批量方法
Secrets.SecretsConfigurationOptions / SecretsConfigurationBuilderExtensions / SecretsConfigurationSource密钥配置源的选项、AddXiHanSecrets(store, options) 与配置源
ConfigurationCapabilities能力 ID 常量 Remote、Secrets 与接口版本 Version
ConfigurationErrors错误码常量
ConfigurationMessages消息键常量与本包的消息目录 Catalog

使用示例 ​

把远程配置挂到 Orders 节下,并把一个密钥映射成绑定的连接串。Nacos 客户端可以换成任何 IRemoteConfigurationClient 实现,vault 是应用自己的 ISecretStore:

csharp
using XiHan.Framework.Configuration.Abstractions.Remote;
using XiHan.Framework.Configuration.Abstractions.Secrets;
using XiHan.Framework.Configuration.Nacos;

var builder = WebApplication.CreateBuilder(args);
var nacosOptions = NacosOptions.FromConfiguration(builder.Configuration);
var vault = new DirectorySecretStore("/run/secrets");

builder.Configuration
    // 工厂重载:客户端归配置源,随配置根释放;传入现成客户端时由调用方持有并负责释放
    .AddXiHanRemoteConfiguration(() => NacosConfigurationClient.Create(nacosOptions), new RemoteConfigurationOptions("orders") { Prefix = "Orders", PollInterval = TimeSpan.FromSeconds(15) })
    .AddXiHanSecrets(vault, new SecretsConfigurationOptions(new Dictionary<string, SecretName>
    {
        ["XiHan:Bindings:db:value"] = "orders/db-connection",
    }));

DirectorySecretStore 的完整实现见配置与选项。自己实现的密钥库用一致性套件验证,测试项目引用 XiHan.Framework.Configuration.Conformance:

csharp
using XiHan.Framework.Configuration.Abstractions.Secrets;
using XiHan.Framework.Configuration.Conformance.Secrets;

namespace Orders.Secrets.Tests;

public sealed class DirectorySecretStoreConformanceTests : SecretStoreConformance
{
    protected override Task<SecretStoreFixture> CreateFixtureAsync() => Task.FromResult<SecretStoreFixture>(new Fixture());

    private sealed class Fixture : SecretStoreFixture
    {
        private readonly string _root = Directory.CreateTempSubdirectory().FullName;

        public override ISecretStore Store => new DirectorySecretStore(_root);

        // 套件经它写入密钥,再从 Store 读回比对
        public override async Task PutAsync(SecretName name, string value, CancellationToken cancellationToken)
        {
            var path = Path.Combine(_root, name.Value);
            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            await File.WriteAllTextAsync(path, value, cancellationToken);
        }
    }
}

单条读取用扩展方法,失败时 Error.Code 是下表里的错误码:

csharp
var secret = await vault.GetAsync("orders/api-key", cancellationToken);
if (secret.TryGetValue(out var value))
{
    // 插值或日志里 value 显示为 "***",真正的值只从 Value 取
    var apiKey = value.Value;
}

行为与限制 ​

  • 首次加载走同步的 IConfigurationProvider.Load:取值在线程池上执行并等待 LoadTimeout。超时与客户端 / 密钥库抛出的异常都归为 CONFIGURATION_UNAVAILABLE_001(远程配置)或 CONFIGURATION_UNAVAILABLE_002(密钥),原因分别是 load timeout 与异常消息。
  • 远程配置首次加载失败时先回调 OnError;Optional = false 时随后抛出 FrameworkException,错误码是失败结果的码,客户端抛出的异常是它的内部异常;Optional = true 时该源为空,照常按 PollInterval 轮询,第一次成功即装入。轮询中客户端返回失败或抛出异常,都只回调 OnError,保留最后一份成功的数据;拿到的文档版本与当前相同时不触发重载。
  • 密钥首次读取中任一项失败且 Optional = false 时抛出 FrameworkException,消息里列出失败的配置键。整体失败(超时、密钥库抛出、返回的条数与请求不一致)回调一次 CONFIGURATION_UNAVAILABLE_002,Optional = false 时随后抛出,为 true 时该源为空。不论首次成败,RefreshInterval 非空时都重读:失败的项保留旧值,整体失败保留全部旧值并回调。
  • OnError 自己抛出的异常被丢弃:不改变首次加载按 Optional 得出的结果,也不终止轮询或重读。
  • 遮蔽只作用于 SecretValue:映射进 IConfiguration 的值是原始字符串。
  • 配置源随配置根释放:释放时停止轮询与重读。工厂重载创建的客户端在轮询停止之后一并释放(实现 IDisposable 时);每次构建 Provider 各建一个,同一构建器构建多次、ConfigurationManager 改动属性或源而重建全部 Provider 时,不会有 Provider 拿到别人已释放的客户端。传入现成客户端的重载不释放它。
错误码常量含义
CONFIGURATION_NOTFOUND_001NamespaceNotFound远程配置的命名空间不存在
CONFIGURATION_NOTFOUND_002SecretNotFound密钥不存在
CONFIGURATION_UNAVAILABLE_001RemoteUnavailable远程配置不可达或超时
CONFIGURATION_UNAVAILABLE_002SecretsUnavailable密钥库不可达或超时
CONFIGURATION_DENIED_001SecretDenied没有读取该密钥的权限
CONFIGURATION_VALIDATION_001DocumentInvalid远程文档不是声明的格式

选项值越界、密钥名不合法、命名空间为空属于编程错误,构造时抛出 ArgumentException 或 ArgumentOutOfRangeException。

相关 ​

Released under The MIT License