XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

XiHan.Framework.MultiTenancy.Abstractions ​

多租户的抽象包:定义租户信息、租户存储、租户解析器、Tenant 阶段中间件与租户分片契约,能力 multitenancy.context@1 与 multitenancy.sharding@1 由多租户 Provider 提供。实现自己的租户存储或分片解析器时引用它。

项内容
包 IDXiHan.Framework.MultiTenancy.Abstractions
环层Ring 2(抽象)
能力定义 multitenancy.context@1、multitenancy.sharding@1(MultiTenancyCapabilities),本包不提供
特性入口—
配置节XiHan:MultiTenancy(MultiTenancyOptions,由 Provider 读取)
依赖XiHan.Framework.Kernel、XiHan.Framework.Localization.Abstractions、Microsoft.Extensions.DependencyInjection.Abstractions
AOT兼容
稳定性Stable;分片相关类型(Sharding 命名空间)为 Preview

概述 ​

租户是调用上下文的一部分。TenantResolutionMiddleware 在 Tenant 阶段按注册顺序调用 ITenantResolver,取第一个结果(没有时用默认租户),校验文法,按选项核对 ITenantStore(存在且启用),然后写进安全上下文 SecurityContext.Tenant;容器里有 ITenantShardResolver 时再解析分片写进调用上下文。失败以结果返回,不抛异常。没有环境态的租户访问器,读租户用 context.GetSecurity().Tenant。

本包还带两个解析器(主体声明、Baggage)与从租户属性推出分片的缺省解析器 TenantStoreShardResolver,多租户 Provider 直接注册它们,只实现存储。持久化 Provider 经 TenantShardBinding.Resolve 读分片选连接,管道外的作用域经 TenantShardScope 显式绑定分片(ADR-0057)。

何时使用 ​

  • 写自己的租户存储(ITenantStore)或分片解析器(ITenantShardResolver)时引用本包,并用 MultiTenancy.Conformance 验证。
  • 业务代码要读分片、在管道外绑定分片、校验租户 ID 时引用本包。
  • 只是启用多租户时直接装 MultiTenancy.InMemory 或 MultiTenancy.Database,它们会带上本包。

安装与启用 ​

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

当前是预览版,安装要带 --prerelease;从源码或本地包源引用见快速上手。本包没有特性入口,Profiles.Standard 与 Profiles.Full 已经引用它。

配置 ​

MultiTenancyOptions.FromConfiguration 读取 XiHan:MultiTenancy 下的五个键:

键类型缺省说明
DefaultTenantstring无解析器都没给出值时使用,同样经过文法与存储校验;空字符串表示清除
RequireTenantboolfalse没有租户的调用返回 MULTITENANCY_VALIDATION_002
ValidateAgainstStorebooltrue租户必须在存储中存在且启用;关闭后只校验文法
TenantClaimstringtenantClaimTenantResolver 读取的声明类型
ShardCacheSizeint10000TenantStoreShardResolver 按租户缓存分片结果的条目上限,满时整体清空;必须是正整数
json
{
  "XiHan": {
    "MultiTenancy": {
      "DefaultTenant": "acme",
      "RequireTenant": false,
      "ValidateAgainstStore": true,
      "TenantClaim": "tenant"
    }
  }
}

主要类型 ​

类型说明
Tenants.TenantInfo租户:Id、DisplayName、IsEnabled(缺省 true)、Properties(键区分大小写)
Tenants.TenantIdentifier租户 ID 文法 ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$:IsValid、Validate、MaxLength(64)
Tenants.ITenantStore租户存储:FindAsync、ListAsync;ID 区分大小写,基础设施失败抛异常
Resolution.ITenantResolver从调用上下文解析租户 ID 候选,只解析不校验
Resolution.ClaimTenantResolver读已认证主体的租户声明(第一个值);未认证主体跳过
Resolution.BaggageTenantResolver读 Baggage xihan.tenant,由进程内建上下文的地方放进去;HTTP、gRPC、MCP 入站不收这个键(ADR-0121)
Resolution.TenantResolutionMiddlewareTenant 阶段的中间件
MultiTenancyOptions选项,见上表
Sharding.ITenantShardResolver分片解析器:ResolveAsync → Result<Maybe<TenantShard>>、Invalidate(tenantId?)
Sharding.TenantShard分片:TenantId、ConnectionString、Provider(转小写,可空)、Properties
Sharding.TenantStoreShardResolver从租户属性推出分片的缺省解析器,按租户有界缓存;FromProperties(tenant) 不带缓存地推出单个租户的分片
Sharding.TenantShardKeys租户属性键 ConnectionString、DatabaseProvider、前缀 Shard.,绑定属性键 tenant、provider
Sharding.TenantShardInvocationExtensionsGetTenantShard() / SetTenantShard(shard):调用上下文里的分片
Sharding.TenantShardBindingResolve(scopedServices, bindingName, bindingType, fallback):持久化 Provider 创建客户端时取分片连接绑定
Sharding.TenantShardScope作用域服务,管道外显式绑定分片:BindTenantAsync、Bind、Shard、IsConnected
MultiTenancyCapabilities能力 ID 常量 Context、Sharding 与 Version(1)
MultiTenancyErrors / MultiTenancyMessages错误码与消息键常量,消息目录 MultiTenancyMessages.Catalog

使用示例 ​

自定义租户存储,例如从应用已有的组织服务读租户:

csharp
using XiHan.Framework.MultiTenancy.Abstractions.Tenants;
using XiHan.Framework.Primitives;

public sealed class FixedTenantStore : ITenantStore
{
    private readonly Dictionary<string, TenantInfo> _tenants = new(StringComparer.Ordinal)
    {
        ["acme"] = new TenantInfo { Id = "acme", DisplayName = "Acme Corp" },
        ["globex"] = new TenantInfo { Id = "globex", DisplayName = "Globex", IsEnabled = false },
    };

    public ValueTask<Maybe<TenantInfo>> FindAsync(string tenantId, CancellationToken cancellationToken)
        => ValueTask.FromResult(_tenants.TryGetValue(tenantId, out var tenant) ? Maybe.Some(tenant) : Maybe.None<TenantInfo>());

    public ValueTask<IReadOnlyList<TenantInfo>> ListAsync(CancellationToken cancellationToken)
        => ValueTask.FromResult<IReadOnlyList<TenantInfo>>([.. _tenants.Values.OrderBy(static t => t.Id, StringComparer.Ordinal)]);
}

多租户 Provider 以 TryAddSingleton<ITenantStore> 注册自己的存储,应用在装配前登记 services.AddSingleton<ITenantStore, FixedTenantStore>() 即可顶替;解析器、中间件与分片解析器沿用 Provider 注册的。

在处理器里读分片,例如按租户库决定是否执行某项迁移:

csharp
using XiHan.Framework.MultiTenancy.Abstractions.Sharding;
using XiHan.Framework.Primitives;

public static class ShardInfo
{
    // 共库租户与无租户的调用没有分片
    public static string Describe(InvocationContext context)
        => context.GetTenantShard().TryGetValue(out var shard)
            ? $"{shard.TenantId} -> {shard.Provider ?? "default"}"
            : "shared";
}

管道外的作用域绑定分片的完整示例见多租户 · 管道外绑定分片。

行为与限制 ​

  • 解析顺序就是 ITenantResolver 的注册顺序。Provider 先注册 ClaimTenantResolver 再注册 BaggageTenantResolver,已认证主体所属的租户优先于调用方自报的租户;主体没有租户声明时 Baggage 生效,中间件只核对文法与存储,不判断主体是否属于该租户。
  • 默认租户同样经过文法与存储校验;关闭存储核对后安全上下文里没有显示名。
  • 分片解析失败(租户存储不可达)返回 MULTITENANCY_UNAVAILABLE_002,调用失败,不降级为共库。
  • TenantStoreShardResolver 缓存"无分片"与未知租户的结果;条目数达到 ShardCacheSize 时整体清空,不逐条淘汰;租户连接变更后调用 Invalidate。
  • Baggage 里的租户只来自进程内建上下文的地方(消息派发、作业、工作流、命令行宿主);HTTP、gRPC、MCP 的网络入站不收这个键(ADR-0121),这些调用方的租户只来自认证。CloudEvents 入站的 xihantenant 经消息派发进来,由宿主给端点加的权限把关。当前租户经 context.GetSecurity().Tenant 读取,没有环境态访问器。
  • 分库的表结构由应用管理;租户属性 ConnectionString 有值即分库,Shard.<key> 透传。
  • TenantShardScope 必须在作用域首次创建持久化客户端之前绑定;之后再绑定或改绑另一分片抛出 InvalidOperationException,同一分片重复绑定无副作用。BindTenantAsync 不看租户是否启用、不受应用状态影响;没有分片解析器时成功且不绑定。
错误码含义
MULTITENANCY_VALIDATION_001租户 ID 不合法
MULTITENANCY_VALIDATION_002调用必须带租户但没有解析到
MULTITENANCY_NOTFOUND_001租户不存在
MULTITENANCY_DENIED_001租户已禁用
MULTITENANCY_UNAVAILABLE_001租户存储容量已满(内存实现)
MULTITENANCY_UNAVAILABLE_002租户分片无法解析(租户存储不可达),reason 只写异常类型的全名,完整异常进日志(ADR-0115)

六个消息键(multitenancy.tenant.*、multitenancy.store.capacity_exceeded、multitenancy.shard.unavailable)在本包的 Localization/<区域>.json 里有 10 种区域的文案,Provider 装配时注册 MultiTenancyMessages.Catalog。

相关 ​

Released under The MIT License