Galosys.Foundation.Core 26.10.29.1

dotnet add package Galosys.Foundation.Core --version 26.10.29.1
                    
NuGet\Install-Package Galosys.Foundation.Core -Version 26.10.29.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Galosys.Foundation.Core" Version="26.10.29.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Galosys.Foundation.Core" Version="26.10.29.1" />
                    
Directory.Packages.props
<PackageReference Include="Galosys.Foundation.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Galosys.Foundation.Core --version 26.10.29.1
                    
#r "nuget: Galosys.Foundation.Core, 26.10.29.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Galosys.Foundation.Core@26.10.29.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Galosys.Foundation.Core&version=26.10.29.1
                    
Install as a Cake Addin
#tool nuget:?package=Galosys.Foundation.Core&version=26.10.29.1
                    
Install as a Cake Tool

Galosys.Foundation.Core

成熟度: 🟢 稳定 — 生产可用,测试充分,活跃维护

Galosys.Foundation 核心库,提供统一响应模型、DDD 实体基类、扩展方法、安全加密、消息总线、模块化机制等基础能力。

特性一览

领域 功能
统一响应 UnifiedResponse 成功/失败/异常/处理中
DDD 实体 Entity / FullEntity / TenantEntity / AppEntity + 领域事件
ID 生成 Snowflake、LocalGenerator、Nanoid、Ulid、Uuid
分页 PagedRequest / PagedResult<T> OFFSET 浅翻页 + KeysetPagedRequest / KeysetPagedResult<T> 游标深翻页(替换旧 PageQry / PageOutput,破坏性变更)
消息 事件订阅/发布、IMessageBus + Outbox 引擎
COLA 架构 Executor 模板方法、ExtensionPoint 扩展点、PluginRegistry
CQRS Lite ICommand<TResult> / ICommandHandler<,> / ICommandBus 接口 + CommandBus 反射编译实现 + AddCqrs() 一键注册 + 命令级 Telemetry + IValidator<T> 校验 + Castle AOP 横切
Resilience 弹性 重试/熔断/超时/隔离/限流/降级(Polly 实现)
State Machine 三泛型状态机 + Builder DSL + 持久化
Rules Engine 规则引擎 + 构建器 + 回退链
分布式锁 IDistributedLock 抽象 + 内存实现
分布式幂等 IDistributedIdempotence 接口
告警 IAlarmer 告警日志基础设施
AI 集成 通义千问 Embedding
Excel 操作 IXlsHelper 流式读/写/模板填充
数据结构 BloomFilter、ConcurrentRunner、ChannelTemplate、责任链
工具类 Env、ServiceLocator、Poco、Tree、ITimeProvider、数据脱敏、签名等
模块化 编译时 SourceGen + 运行时双模式模块发现
DI 上下文 ITenantContext / IAppContext / IUserContext
WebSocket Layer 1 ExponentialBackoff 退避函数 + ArrayPoolWebSocketBuffer 接收缓冲 + 帧/关闭码中文描述

1. 统一响应模型

// 成功响应
var response = UnifiedResponse.Succeed(data, "操作成功");

// 失败响应
var response = UnifiedResponse.Fail("ERROR_CODE", "操作失败");

// 异常响应
var response = UnifiedResponse.Error("ERROR_CODE", exception);

// 处理中
var response = UnifiedResponse.Processing("PROCESSING_CODE");

2. 实体与 DDD 模式

// 完整实体(含创建者、修改者、软删除)
public class User : FullEntity<long>
{
    public string Name { get; set; }
}

// 基础实体
public class Entity
{
    public IReadOnlyCollection<ApplicationMessage> DomainEvents { get; }
    protected void AddEvent(ApplicationMessage @event) { }
}
实体基类矩阵
Entity (abstract, 领域事件)
  └── Entity<TID> (abstract, [Key, SnowflakeId] Id)
       ├── FullEntity<TID> (abstract, IDeletable + ICreator + ILastModifier)
       │    ├── TenantEntity<TID>      : FullEntity<TID>, IMultiTenancy
       │    ├── AppEntity<TID>         : FullEntity<TID>, IMultiApplication
       │    └── TenantAppEntity<TID>   : FullEntity<TID>, IMultiTenancy, IMultiApplication
基类 适用场景 示例
FullEntity<TID> 全局基础数据,不分租户和应用 地区、字典、系统配置
TenantEntity<TID> 仅分租户 用户、角色、权限
AppEntity<TID> 仅分应用 菜单、API 接口
TenantAppEntity<TID> 租户 + 应用 租户应用配置

实体可混入 IConcurrency 实现乐观并发控制:

public class SysUser : TenantEntity<long>, IConcurrency
{
    [Timestamp]
    public byte[] RowVersion { get; protected set; } = null!;
}

L2 形态 — 领域事件自动分发

聚合根继承 Entity 基类即具备 DomainEvents 收集能力(AddEvent / ClearEvents,实现 IHasDomainEvents 标记接口)。启用 AddEfCoreOutboxStore<T>() 后,EF SaveChanges 成功时拦截器自动把事件发布到进程内订阅者(IApplicationListener<T>)。

// 聚合根:业务方法收集事件
public sealed class Order : FullEntity<long>
{
    public Order Place(long customerId, List<OrderItem> items)
    {
        ...
        AddEvent(new OrderPlaced(this));
        return this;
    }
}

// Handler:无任何事件相关标注,正常持久化即可
[Transactional]
public sealed class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand, long>
{
    public async Task<long> HandleAsync(PlaceOrderCommand cmd, CancellationToken ct = default)
    {
        var order = Order.Place(...);
        _db.Orders.Add(order);
        await _db.SaveChangesAsync(ct);   // ← 保存成功后拦截器自动发布 OrderPlaced
        return order.Id;
    }
}

// 订阅方:实现 IApplicationListener<T> 即自动接收(AddApplicationMessagePublisher 扫描注册)
public sealed class OrderPlacedListener : IApplicationListener<OrderPlaced>
{
    public Task<bool> HandleAsync(OrderPlaced e)
    {
        // 读模型更新 / 进程内通知
        return Task.FromResult(true);
    }
}

注册:services.AddEfCoreOutboxStore<ApplicationDbContext>();(一并启用进程内事件分发 + 跨服务 Outbox;未启用 Outbox store 时也可手动 TryAddSingleton<DomainEventSaveChangesInterceptor>() 并确保发布器已注册)

跨服务集成事件不走本机制——Handler 内显式 IMessageBus.PublishAsync(routingKey, event)(Outbox → MQ,事务一致)。


3. ID 生成

// 雪花算法 ID
var id = SnowflakeIdGenerator.Instance.NextId();

// 本地 ID 发生器
var id = LocalGenerator.NextId();
var couponKey = LocalGenerator.NextCouponKey();
var seqKey = LocalGenerator.NextSeqKey();
var guid = LocalGenerator.NextGuid();

此外支持多种 ID 策略(通过特性标注实体字段):

[Nanoid]      // Nanoid 字符串 ID
[Ulid]        // ULID 有序唯一标识
[Uuid]        // UUID 标识

4. 分页

BREAKING(自 26.10 版本起):旧 PageQry / PageOutput 已被 PagedRequest / PagedResult<T> 直接替换,无 [Obsolete] 双轨。迁移对照:

旧 PageQry 新 PagedRequest
PageNum PageNo
PageSize PageSize
旧 PageOutput 新 PagedResult<T>
Rows (IEnumerable) Data (IReadOnlyList<T>)
Total TotalRecords
Pages TotalPages(计算)
HasNext HasNextPage(计算)
HasPrevious HasPreviousPage(计算)

OFFSET 浅翻页(PagedRequest / PagedResult<T>)

PagedRequest 用于浅翻页场景,PageSize 硬上限 MaxPageSize = 100(Set 自动截断;0 或负数视为 30):

var request = new PagedRequest
{
    PageNo = 1,
    PageSize = 20,        // > 100 会被截断为 100
    SortBy = "name desc,price asc",
    Filters = new List<FilterItem> { new() { Field = "name", Op = "contains", Value = "手机" } },
    Fields = "id,name"
};

PagedResult<T> 数据集合为不可变 IReadOnlyList<T>,TotalPages / HasNextPage / HasPreviousPage 为计算属性:

var result = new PagedResult<T>(items, pageNo: 1, pageSize: 20, totalRecords: 100);
// TotalPages == 5, HasNextPage == true, HasPreviousPage == false

命名族分层:PagedResult<T> 是数据载体层;UnifiedResponse 是 API 响应包装层(Code/Msg/TraceId/Timestamp)。Controller 返回模式:UnifiedResponse.Succeed(pagedResult)。

Keyset 游标深翻页(KeysetPagedRequest / KeysetPagedResult<T>)

深翻页场景(数据量 200K+ / 翻到第 1000 页)用 Keyset 替代 OFFSET Skip/Take,常数时间翻页,避免 LIMIT + OFFSET 深翻页数秒级延迟:

// 请求契约
var request = new KeysetPagedRequest { Cursor = prevPage.NextCursor, Size = 30 };
// 结果契约
var result = new KeysetPagedResult<T>(items: pageItems, nextCursor: next, hasNext: has);
// Items   — 当前页数据
// NextCursor — 下一页游标(base64 编码 + TOrderBy.FullName 类型前缀,跨实体防错位)
// HasNext — 是否还有下一页

约束:排序键 TOrderBy 必须唯一且建索引(典型:自增 long Id / Guid 字典序 / DateTime 复合 Id)。排序键可能重复(如 DateTime)时必须在 WHERE 叠加第二键(如 Id > lastId)做 tie-break。游标类型前缀不匹配时视为无效游标,回退首页查询,不抛异常。查询执行在 EFCore 模块的 KeysetPagingExtensions.KeysetPageAsync<T, TOrderBy>。


5. 单例模式

// 线程安全的延迟初始化单例
var instance = Singleton<MyService>.GetInstance();

// 使用 Lazy<T> 的单例
var instance = LazySingleton<MyService>.Instance;

6. 消息订阅/发布

// 发布消息
await publisher.PublishAsync(new UserCreatedEvent(this));

// 订阅消息
public class UserCreatedListener : ApplicationListener<UserCreatedEvent>
{
    public override Task<bool> HandleAsync(UserCreatedEvent e)
    {
        return Task.FromResult(true);
    }
}

7. 扩展方法

// DateTime 扩展
var lastDay = dt.LastDayInMonth();
var firstDay = dt.FirstDayInMonth();
var isWeekend = dt.IsWeekend();
var workDate = dt.AddWorkDays(3);
var unixMs = dt.ToUnixTimeMilliseconds();
var unixSec = dt.ToUnixTimeSeconds();
var datetime = timestamp.ToDateTime();

// String 扩展
var compressed = "content".ToGZipCompress();
var decompressed = compressed.ToGZipDecompress();

// Object 扩展
var result = obj.GetAsyncResult<Task<string>>();

// IQueryable 扩展
queryable.WhereIf(condition, x => x.IsActive)
         .Page(pageIndex, pageSize);

// 集合扩展
list.Shuffle();
list.TopologicalSort(selector);
list.Slice(5);
list.BinaryInsertSort(item, comparer);

8. 安全加密

// MD5 加密
var hashed = SecurityTemplate.Md5Encrypt(original);

// DES 加解密
var encrypted = SecurityTemplate.DesEncrypt(original);
var decrypted = SecurityTemplate.DesDecrypt(encrypted);

// AES 加解密
var encrypted = SecurityTemplate.AesEncrypt(original, key);
var decrypted = SecurityTemplate.AesDecrypt(encrypted, key);

// RSA 加解密
var encrypted = SecurityTemplate.RSAEncrypt(original, publicKey);
var decrypted = SecurityTemplate.RSADecrypt(encrypted, privateKey);

// 许可证生成与验证
var license = SecurityTemplate.GenerateLicense(info);
var valid = SecurityTemplate.ValidateLicense(license);

9. 动态代理

class HttpProxy : ServiceProxy
{
    protected override void Proceed(InvocationContext context)
    {
        var http = context.ServiceProvider.GetRequiredService<IHttpClientFactory>().CreateClient();
        context.SetReturnValue(http.GetStringAsync("/posts/1"));
    }
}

var instance = ServiceProxyFactory.CreateProxy(typeof(ITypicodeClient), typeof(HttpProxy), sp) as ITypicodeClient;

10. 进程管理

// 启动进程
var p = new Process().Start("cmd.exe", runAsAdministrator: true);

// 打开浏览器
p = new Process().Browser("https://example.com");

// 打开资源管理器
p = new Process().Explore("c://");

// 执行脚本
p = new Process().Script("dotnet.exe", "info");

11. HTTP 客户端

services.AddHttpClient<IPlaceholderGateway, PlaceholderGateway>(http =>
{
    http.BaseAddress = new Uri("https://jsonplaceholder");
}).AddServiceDiscovery()
  .AddStandardHedgingHandler();

12. 验证码生成

var code = Random.Shared.GenerateCaptcha();   // 验证码
var sms = Random.Shared.GenerateSms();         // 短信验证码

13. 应用程序启动器

实现 IApplicationRunner 并标注 [Service],框架会在 Host 触发 ApplicationStarted 后自动执行; 多个 runner 通过 [Order(n)] 升序串行执行,单个 runner 失败只记录日志、不阻断后续 runner。

[Service]
[Order(10)]
public class MyRunner : IApplicationRunner
{
    public Task RunAsync(ApplicationArguments args, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
}

触发链:Bootstrapper → ApplicationStartedMessage → ApplicationStartedListener → IApplicationRunner。 发布与监听注册均由 ApplicationMessagePublisher 管理,channel 首次物化时自动启动监听循环。


14. PluginRegistry(插件注册表)

基于策略路由模式的基础设施,根据请求自动匹配对应插件实现。适用于短信网关、支付渠道、物流商等多 provider 场景。

public interface ISmsPlugin : Plugin<SmsRequest>
{
    Task SendAsync(SmsRequest request);
}

internal class AliyunSmsPlugin : ISmsPlugin
{
    public bool Supports(SmsRequest request) => request.Provider == "aliyun";
    public Task SendAsync(SmsRequest request) { /* ... */ }
}

// DI 注册
services.AddTransient<ISmsPlugin, AliyunSmsPlugin>();

// 使用
var registry = serviceProvider.GetRequiredService<PluginRegistry<ISmsPlugin, SmsRequest>>();
var plugin = registry.GetPluginFor(request);
await plugin.SendAsync(request);
模式 行为
GetPluginFor 单匹配 — 返回第一个匹配插件
GetAllPluginsFor 多匹配 — 返回所有匹配插件(广播场景)
PluginResolutionMode.Scoped 默认,创建新 DI Scope
PluginResolutionMode.Singleton 从根容器直接获取

15. Executor 模板方法

提供命令执行管道(仅异步),按顺序执行:前置钩子 → 参数校验 → 业务逻辑 → 后置钩子,统一返回 Task<UnifiedResponse>。AddCola() 会自动扫描并注册(Scoped)所有 IExecutor<TCommand> 实现,无需手工注册。

// 注册(自动扫描当前运行时程序集中的扩展点与 IExecutor<TCommand> 实现)
services.AddCola();

// 定义命令(重写异步校验)
public class OrderCreateCmd : CommandBase
{
    public string OrderNo { get; set; }
    public override Task<ValidationResult> ValidateAsync(CancellationToken ct = default) { /* ... */ }
}

// 实现 Executor(仅需实现异步业务逻辑)
public class OrderCreateExecutor : ExecutorBase<OrderCreateCmd>
{
    public OrderCreateExecutor(ILogger<ExecutorBase<OrderCreateCmd>> logger, IServiceProvider sp) : base(logger, sp) { }

    protected override Task<UnifiedResponse> DoExecuteAsync(OrderCreateCmd command, CancellationToken ct)
    {
        return Task.FromResult(UnifiedResponse.Succeed(new { OrderId = Guid.NewGuid() }));
    }
}

// 显式注入并异步执行
var executor = sp.GetRequiredService<IExecutor<OrderCreateCmd>>();
UnifiedResponse result = await executor.ExecuteAsync(cmd);
阶段 方法 说明
前置钩子 OnBeforeExecuteAsync 记录日志,可重写
参数校验 command.ValidateAsync() 失败返回 VALIDATION_ERROR
业务逻辑 DoExecuteAsync 子类必须实现
后置钩子 OnAfterExecuteAsync 记录日志,可重写
业务异常 HandleBusinessException 捕获 BizException
系统异常 HandleSystemException 返回 SYSTEM_ERROR

16. CQRS Lite

轻量级 Command/Query 分离抽象——与 Cola Executor 双轨并行,按"业务场景复杂度"选择使用。

双轨定位:简单 CRUD / 单一横切 → Cola Executor;≥3 横切 / Outbox 原子保证 / 领域事件衔接 → CQRS Dispatcher。详见 docs/designs/capability-gap-and-action-plan.md 附录 A。

16.1 接口契约

// 命名空间:Galosys.Foundation.Core(命名空间寄生:依赖 ICommand<TResult> 同处 Core)
public interface ICommand<TResult> : ICommand { }    // 继承富接口(RequestId/UserId/TraceId/Validate)

// 命名空间:Galosys.Foundation.Core
public interface ICommandHandler<in TCommand, TResult>
    where TCommand : ICommand<TResult>
{
    Task<TResult> HandleAsync(TCommand command, CancellationToken ct = default);
}

// 命名空间:Galosys.Foundation.Core(命令总线:dispatcher 仅承载 Command 一侧,命名规范对齐 MediatR ISender / Brighter IAmACommandProcessor / MassTransit IBus)
public interface ICommandBus
{
    Task<TResult> SendAsync<TResult>(ICommand<TResult> command, CancellationToken ct = default);
}

16.2 使用示例

// 1) 定义命令(继承 CommandBase,自动获得 ICommand 富接口成员)
public sealed class CreateOrderCommand : CommandBase, ICommand<OrderId>
{
    public string Sku { get; set; } = string.Empty;
    public int Quantity { get; set; }
}

public readonly record struct OrderId(long Value);

// 2) 实现 Handler
public sealed class CreateOrderHandler : ICommandHandler<CreateOrderCommand, OrderId>
{
    private readonly IOrderRepository _repository;

    public CreateOrderHandler(IOrderRepository repository) => _repository = repository;

    public async Task<OrderId> HandleAsync(CreateOrderCommand cmd, CancellationToken ct = default)
    {
        var order = new Order { Sku = cmd.Sku, Quantity = cmd.Quantity };
        await _repository.AddAsync(order, ct);
        return new OrderId(order.Id);
    }
}

// 3) DI 注册(一行扫所有 ICommandHandler<,>;也可省略——AddCore() 已自动调用 AddCqrs)
services.AddCqrs();

// 4) 注入并使用
public class OrderController(ICommandBus bus)
{
    public async Task<OrderId> Create(CreateOrderCommand cmd)
    {
        return await bus.SendAsync(cmd);   // 返回裸 TResult(Controller 自行包装 UnifiedResponse)
    }
}

16.3 Dispatcher 行为

行为 实现细节
首次调用 通过 Expression.Lambda 编译 Func<IServiceProvider, object, CancellationToken, Task<TResult>>,固化到 ConcurrentDictionary<Type, Func<>>
后续调用 直接字典查询,零反射
Handler 解析 每次 SendAsync 创建独立 DI scope,从 scope.ServiceProvider 解析 ICommandHandler<TCommand, TResult>;handler 可安全注入 Scoped 依赖(如 DbContext),scope 在 dispatch 完成后释放
异常透传 Handler 抛出的异常完全透传(不包装、不记录日志)
未注册 Handler 抛 InvalidOperationException,消息含命令 FullName 与 services.AddCqrs() 注册提示

16.4 与 Executor 的取舍

维度 Cola Executor CQRS Dispatch
返回类型 Task<UnifiedResponse> Task<TResult>
横切挂载 继承基类 + 重写 OnBefore/OnAfter Castle AOP Attribute([Transactional] / [Retryable])
异常处理 基类 catch + 翻译 UnifiedResponse.Fail 透传
BizScenario 扩展 ✅ 内置 ❌ 无
命令→Handler 1:1 ✅ 显式 Executor 注入 ✅ CommandBus 按命令类型解析

16.5 当前阶段(Phase 1 + 1.1 + Phase 2)

  • ✅ 接口定义(ICommand<TResult> / ICommandHandler<,> / ICommandBus),全部位于 Galosys.Foundation.Core 命名空间
  • ✅ CommandBus 反射编译实现 + AddCqrs() DI 注册
  • ✅ Phase 2:命令级 Telemetry(ActivitySource("Galosys.Foundation.Cqrs", "1.0.0")) + IValidator<T> 直接注入校验 + Castle AOP 横切([Transactional] / [Retryable])。不引入 IPipelineBehavior 中间件链——横切统一走框架既有 Castle AOP,校验在 Handler 内显式调用
  • ✅ Phase 2 端到端示例:samples/Dev.ConsoleApp/CQRS_SAMPLE.md
  • ✅ Phase 3:领域事件 EF SaveChanges 拦截器自动分发(DomainEventSaveChangesInterceptor,随 AddEfCoreOutboxStore<T>() 启用);集成事件维持 IMessageBus.PublishAsync 处理器决策
  • ⏳ Phase 4:注入式查询类(OrderQueries)
  • ⏳ Phase 5:Native AOT 源生成器 + samples

16.6 命令级链路追踪(Telemetry)

CommandBus.SendAsync<TResult> 内置 OpenTelemetry 链路追踪,无需任何中间件:

// DI 注册(默认 EnableCqrsTelemetry = true,可显式关闭)
services.AddCqrs(o => o.EnableCqrsTelemetry = true);

// OpenTelemetry 侧需注册同名 Source 才能采集到 Span
builder.Services.AddOpenTelemetry()
    .WithTracing(t => t.AddSource("Galosys.Foundation.Cqrs"));
  • 每个 SendAsync 调用创建一个 Activity,Span 名称为 cqrs.sendAsync/{命令运行时类型名}
  • Tag:cqrs.command = 命令 FullName,cqrs.result_type = typeof(TResult).Name
  • Handler 抛异常时 Span 状态标记为 Error
  • 全局开关另可经 CommandBusTelemetry.Enabled 静态属性控制

16.7 业务规则校验(Validation)

校验抽象独立于 CQRS,命名空间寄生于 Microsoft.Extensions.Validation(.NET 10 官方包,10.0.11),默认实现 DataAnnotationsValidator<T> 桥接官方源生成器 + BCL System.ComponentModel.DataAnnotations 回退:

// 1) 命令属性打 DataAnnotations 特性(BCL,无需额外 NuGet)
//    或打 [ValidatableType] 走官方源生成器
public sealed class CreateOrderCommand : CommandBase, ICommand<OrderId>
{
    [Required] public string Sku { get; set; } = string.Empty;
    [Range(1, int.MaxValue)] public int Quantity { get; set; }
}

// 2) Handler 构造注入 IValidator<T>,HandleAsync 开头显式校验(fail-fast)
public sealed class CreateOrderHandler : ICommandHandler<CreateOrderCommand, OrderId>
{
    private readonly IValidator<CreateOrderCommand> _validator;
    public CreateOrderHandler(IValidator<CreateOrderCommand> validator) => _validator = validator;

    public async Task<OrderId> HandleAsync(CreateOrderCommand cmd, CancellationToken ct = default)
    {
        var result = await _validator.ValidateAsync(cmd, ct);
        if (!result.IsValid)
            throw new BizException(string.Join("; ", result.Errors.SelectMany(e => e.Errors)));
        // ...
    }
}

通用入口 AddValidationCore()(寄生 Microsoft.Extensions.DependencyInjection):

// Program.cs / Startup.cs
builder.Services.AddValidationCore(options =>
{
    options.MaxDepth = 32; // 嵌套校验深度上限
});
// 等价于:services.AddValidation(options => ...);  // 官方
//          services.TryAddTransient(typeof(IValidator<>), typeof(DataAnnotationsValidator<>));  // 我们

三种典型使用姿势(不与 CQRS 耦合):

// 姿势 1:仅 CQRS(AddCqrs 已隐含注册 IValidator<T> 默认实现)
services.AddCqrs();

// 姿势 2:CQRS + 自定义 ValidationOptions
services.AddValidationCore(o => o.MaxDepth = 64);
services.AddCqrs();

// 姿势 3:非 CQRS 业务(任意场景)
services.AddValidationCore();
services.AddTransient<IValidator<MyDto>, MyCustomValidator>();
  • AddCqrs 自动注册开放泛型 IValidator<> → DataAnnotationsValidator<>(TryAddTransient 幂等,可被 AddValidationCore / 自定义注册覆盖)
  • 引擎可替换:注册具体 IValidator<MyDto> 实现可覆盖默认;上层可叠加 FluentValidation 等

⚠️ BREAKING 变更(自 26.9 版本起):

  • IValidator<T> / IValidationResult 命名空间由 Galosys.Foundation.Core 迁至 Microsoft.Extensions.Validation
  • 自定义 ValidationError(MemberName, ErrorMessage) 已删除;IValidationResult.Errors 元素类型改用官方 Microsoft.Extensions.Validation.ValidationErrorContext:
旧字段(已删除) 新字段(官方) 说明
MemberName Name 字段或参数名
ErrorMessage Errors(IReadOnlyList<string>) 多消息列表(原单字符串 → 列表)
— Path 嵌套属性路径(新增)
— Container 被校验对象引用(新增;JSON 序列化时需排除避免循环引用)

调用方需将 result.Errors.Select(e => e.ErrorMessage) 改为 result.Errors.SelectMany(e => e.Errors)。

序列化注意:ValidationErrorContext.Container 持有被校验对象引用,JSON 序列化时必须排除;可使用 ValidationErrorContextExtensions.WithoutContainer() 投影(寄生扩展位于 Microsoft.Extensions.Validation 命名空间)。

16.8 完整示例与选型

  • 端到端示例:samples/Dev.ConsoleApp/CQRS_SAMPLE.md —— 命令 + Handler + Castle AOP 横切 + IValidator<T> 注入校验 + Outbox 自动 flush + OpenTelemetry 链路追踪,非 HTTP 入口([HostedService] Worker)派发命令
  • 何时启用 CQRS:见决策树 docs/designs/capability-gap-and-action-plan.md(附录 A.4 决策指南)。默认推荐 Cola Executor(简单 CRUD / 强一致);仅当需要 Outbox 原子保证、BizScenario 扩展、≥3 个横切、裸 TResult 返回时才选 CQRS Lite。横切一律经 Castle AOP Attribute 挂载,而非 IPipelineBehavior

17. ExtensionPoint(扩展点机制)

基于 COLA 架构的扩展点模式,通过 BizScenario(业务身份)路由到不同的扩展点实现。

public interface IPaymentStrategy : IExtensionPoint
{
    Task<PaymentResult> PayAsync(PaymentRequest request);
}

[Extension(BizId = "default", UseCase = "default", Scenario = "wechat")]
public class WechatPaymentStrategy : IPaymentStrategy { /* ... */ }

// 使用
var scenario = BizScenario.ValueOf("default", "default", paymentType);
var result = await executor.ExecuteAsync<IPaymentStrategy, PaymentResult>(
    scenario, svc => svc.PayAsync(request));

匹配降级策略:精确匹配 → 场景通配 → 用例通配 → 全局默认。

支持批量执行(ExecuteAll / ExecuteAllAsync)和归约(ReduceAsync)。


18. PageQuery(分页查询基类)

var query = new PageQuery
{
    PageIndex = 2,
    PageSize = 10,
};
query.SetOrderDirection("ASC");
int offset = query.Offset; // 10
属性 说明 默认
PageIndex 页码(最小1) 1
PageSize 每页大小(最小1) 10
Offset 偏移量(只读) (PageIndex-1)*PageSize
OrderDirection 排序方向 "DESC"

19. MessageBus + Outbox 引擎

提供 IMessageBus 统一消息入口(Send + Publish),以及 Transactional Outbox 模式保证业务事务与消息发送的原子性。

// 进程内默认总线
await bus.PublishAsync("order.created", order);

// 实现消息处理器
public class OrderCreatedHandler : IMessageHandler<OrderCreatedEvent>
{
    public Task<bool> HandleAsync(OrderCreatedEvent msg) { /* ... */ }
}

// DI 注册
services.AddScoped<IMessageHandler<OrderCreatedEvent>, OrderCreatedHandler>();

Outbox 模式: PendingMsgCol 收集待发送消息,DbContext.DrainPendingOutbox() 在 SaveChanges 时同事务写入 base_outbox_msg 表(列名 snake_case),后台 OutboxRelay 轮询投递。

性能优化(Phase 2): OutboxMessage 新增 Version 字段支持乐观锁,IOutboxStore 新增 TryAcquireBatchAsync / MarkManySentAsync / MarkManyFailedAsync 批量 API,OutboxRelay 改为 TryAcquireBatch → Parallel.ForEachAsync(DOP=4)→ MarkMany 流水线,吞吐目标 200-500/s。

services.AddRabbitMessageBus(options => {
    options.EnableOutbox = true;
    options.Outbox.DefaultMode = "Outboxed"; // 默认走 outbox
});
services.AddEfCoreOutboxStore<MyDbContext>();
配置项 默认值 说明
EnableOutbox true 全局 outbox 开关
TableName "base_outbox_msg" 发件箱表名(列名 snake_case)
SchemaName "base" 数据库 schema
DefaultMode "Outboxed" 路由默认策略:"Outboxed"=走 outbox,"Direct"=直发
MaxRetryCount 3 最大重试次数(0=不限)
BatchSize 500 每次轮询拉取数
PollIntervalMs 500 轮询间隔(ms)
RetentionDays 7 已发送消息保留天数
组件 说明
IMessageBus 统一消息入口
PendingMsgCol AsyncLocal 消息收集器
OutboxSaveChangesInterceptor EF Core 保存拦截器
OutboxRelay 后台轮询投递服务
OrphanedCleanupService 孤儿消息清理
OutboxMeter OpenTelemetry 指标(含 outbox.orphaned.total 未落库丢弃计数)

OutboxMeter 指标一览:

指标名 类型 说明
outbox.sent.total Counter 累计发送成功数
outbox.failed.total Counter 累计发送失败数
outbox.retry.exhausted Counter 重试耗尽丢弃数
outbox.orphaned.total Counter 累计未落库丢弃数 — PublishAsync 后未调 SaveChanges 导致消息丢失时递增
outbox.scan.duration Histogram 单次轮询耗时(ms)
outbox.pending.count ObservableGauge 待发送消息数

20. 上下文服务(DI)

框架内置 ITenantContext、IAppContext、IUserContext 三个 Singleton 上下文服务,基于 AsyncLocal<T> 实现请求级隔离。

public class MyService
{
    public MyService(ITenantContext tenant, IAppContext app, IUserContext user) { }
}

21. 多租户(Multi-Tenancy)

框架提供多层防御的多租户体系:行级隔离 + fail-secure 默认 + 显式系统模式 + 表级与插入级穿透。设计依据 ADR-0004 与 ADR-0005。

21.1 三态上下文(fail-secure 默认)

ITenantContext 有三个互斥状态,未显式初始化时默认安全(fail-secure):

IsSystemContext TenantId 含义 过滤器行为
false null 未初始化 抛 InvalidOperationException
false >0 正常租户 按 TenantId 过滤
true null 系统/总后台 穿透,返回所有租户数据
using Galosys.Foundation.Core;

// 场景1:正常业务(中间件已 SetTenant)
public class OrderService(ITenantContext ctx)
{
    public Task<List<Order>> GetCurrent() => db.Orders.ToListAsync();
    // 过滤器自动按 ctx.TenantId 过滤
}

// 场景2:跨租户查询(系统后台)
public class GlobalAdminService(ITenantContext ctx, DbContext db)
{
    public async Task<List<Order>> GetAllTenantsOrders()
    {
        using (ctx.EnterSystemContext())  // 显式 opt-in
        {
            // IsSystemContext=true,过滤器穿透
            return await db.Orders.ToListAsync();
        }
        // 作用域结束,自动还原
    }
}

// 场景3:未初始化调用 → 抛异常(fail-secure)
public async Task CrashDemo()
{
    // 没有中间件、没有 SetTenant
    var orders = await db.Orders.ToListAsync();
    // InvalidOperationException: 未初始化,调用 Change() 或 EnterSystemContext()
}

21.2 租户元数据 POCO

using Microsoft.Extensions.MultiTenancy;

// 普通租户
var tenant = new Tenant
{
    Id = 1,
    Identifier = "acme",
    Name = "Acme Corporation",
    IsolationMode = TenantIsolationMode.Row,
    IsActive = true
};

// 系统模式 token(不存储于 ITenantStore,仅作占位)
var sys = Tenant.System;  // Identifier = "__system__"

21.3 租户存储与注册

using Microsoft.Extensions.MultiTenancy;

// 注册多租户服务(默认 InMemoryTenantStore + TenantContext)
services.AddTenancy();

// 自定义租户存储(EF Core 实现)
public class EfCoreTenantStore : ITenantStore
{
    public Task<Tenant?> GetByIdentifierAsync(string id, CancellationToken ct = default)
        => db.Tenants.FirstOrDefaultAsync(t => t.Identifier == id, ct);

    public Task<Tenant?> GetByIdAsync(long id, CancellationToken ct = default)
        => db.Tenants.FirstOrDefaultAsync(t => t.Id == id, ct);
}

services.AddSingleton<ITenantStore, EfCoreTenantStore>();
21.3.1 MultiTenancyStoreOptions(可配置目标表)

MultiTenancyStoreOptions 用于覆盖租户元数据表的目标 schema 与表名,默认行为与历史一致(Schema = null,TableName = "mt_tenant"),零破坏。

字段 类型 默认值 说明
Schema string? null schema 名;null/空 → 单参 ToTable(name),非空 → 双参 ToTable(name, schema)
TableName string "mt_tenant" 表名;未配置兜底 MultiTenancyStoreConstants.TableName

ADR-0001 规则 4:表名/列名仍以 MultiTenancyStoreConstants 为单一常量来源,MultiTenancyStoreOptions 仅在消费方显式配置时覆盖。

典型用例:指向平台统一租户表 uc.uc_tenant

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.MultiTenancy;

services.AddEfCoreTenantStore<MyDbContext>(o =>
{
    o.Schema = "uc";
    o.TableName = "uc_tenant";
});

uc.uc_tenant 表由平台侧(UC 团队)负责创建与迁移,本仓库只消费。

21.4 多层穿透机制

using Galosys.Foundation.Core;

// ===== 1. 表级穿透:IGlobalEntity =====
// 系统字典、菜单等全局表:不参与多租户过滤
public class SysDict : Entity, IGlobalEntity  // ★ 不实现 IMultiTenancy
{
    public string Code { get; set; }
    public string Value { get; set; }
    // 无 TenantId 属性
}

// ===== 2. 插入穿透:ITenantInsertOptOut =====
// 系统后台批量导入:手动控制 TenantId,SaveChanges 不自动填充
public class SysUser : FullEntity<long>, IMultiTenancy, ITenantInsertOptOut
{
    public long TenantId { get; set; }   // 必须有,但 SaveChanges 不自动填
    public string Name { get; set; }
}

// 在服务中
public class ImportService(ITenantContext ctx, DbContext db)
{
    public async Task ImportUser(SysUser user)
    {
        user.TenantId = 42;  // 显式指定,不依赖 ctx.TenantId
        db.Users.Add(user);
        await db.SaveChangesAsync();  // 不会覆盖为 ctx.TenantId
    }
}

// ===== 3. 上下文系统模式:EnterSystemContext() =====
public class ReportService(ITenantContext ctx, DbContext db)
{
    public async Task<List<Order>> GetCrossTenantReport()
    {
        using (ctx.EnterSystemContext())
        {
            return await db.Orders.ToListAsync();  // 跨租户
        }
    }
}

// ===== 4. 单次查询绕过:IgnoreQueryFilters() =====
public Task<List<Order>> GetAllIncludingSoftDeleted()
    => db.Orders.IgnoreQueryFilters().ToListAsync();

21.5 选项与迁移开关

using Galosys.Foundation.Core;

// 默认配置:fail-secure(未初始化抛异常)
services.AddTenancy();

// 迁移期配置:暂时关闭 fail-secure(仅旧后台任务需要)
services.Configure<MultiTenancyOptions>(opts =>
{
    opts.ThrowOnUninitialized = false;     // 关闭 fail-secure
    opts.AllowLegacyZeroTenant = true;    // 允许 SetTenant(0) 进入系统模式
});
// 迁移完成后立即移除此配置

21.6 解析策略与中间件

// 自定义解析策略(从 Header 解析)
public class HeaderResolutionStrategy : ITenantResolutionStrategy
{
    private readonly IHttpContextAccessor _accessor;
    public HeaderResolutionStrategy(IHttpContextAccessor accessor) => _accessor = accessor;

    public Task<string?> ResolveIdentifierAsync(CancellationToken ct = default)
        => Task.FromResult(_accessor.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault());
}

// 中间件使用(参见 AspNetCore 包)
app.UseMultiTenancy(opts => opts
    .WithHeader("X-Tenant-Id")           // 优先 Header
    .WithHost("{tenant}.example.com")    // 其次 Host
    .WithStore<InMemoryTenantStore>());  // 自定义存储

21.7 选型决策

场景 推荐机制
静态系统表(字典、菜单) IGlobalEntity
系统后台批量导入 ITenantInsertOptOut
跨租户查询/操作 EnterSystemContext()
单次查询穿透 IgnoreQueryFilters()
正常业务 默认(无标记)

详细决策树参见 docs/designs/multi-tenancy-enhancement.md 附录 A。

21.8 P2 隔离抽象层(Isolation Abstractions)

P1 已落地行级(Row)隔离。P2 引入 Schema / Database / Hybrid 隔离,Core 层先交付隔离抽象层(spec multi-tenancy-p2-isolation-abstractions),不触 EFCore/ORM;Schema / Database / Hybrid 的具体路由分别走 P2-2 / P2-3。

Tenant POCO 新增 3 个 nullable 运行时元数据字段(默认 null,EF Core 映射 Ignore,不落库):

public string? SchemaTemplate             { get; set; }  // 如 "uc_{id:00000000}"
public string? DatabaseTemplate           { get; set; }  // 如 "tenant_{id:00000000}"
public string? IsolationConnectionString  { get; set; }  // Database 模式专用

IsolationModeResolver:无状态服务,集中隔离决策逻辑。输入 (Tenant, MultiTenancyIsolationOptions) 输出 IsolationModeDecision { Mode, IsolationKey }。模板占位符 {id:00000000}(8 位补零,排序友好):

using Microsoft.Extensions.MultiTenancy;

var decision = resolver.Resolve(tenant, options);
// Row      → IsolationKey = null
// Schema   → SchemaTemplate?.Replace(...) ?? options.DefaultSchemaTemplate...
// Database → DatabaseTemplate?.Replace(...) ?? options.DefaultDatabaseTemplate...
// Hybrid   → 返回 Schema 名(连接串由 DatabaseConnectionStringResolver 单独提供)

MultiTenancyIsolationOptions(DI Options,注入顺序 RemoveAll → Configure → AddScoped):

字段 默认值 说明
DefaultSchemaTemplate "tenant_{id:00000000}" Schema 模式默认模板
DefaultDatabaseTemplate "tenant_{id:00000000}" Database 模式默认模板
DatabaseConnectionStringResolver null Func<long, string?>,输入 Tenant.Id 返回连接串;null 时 Database 模式 fail-secure

ISchemaTenantAccessor:按当前租户解析 Schema 名(Schema 隔离必备;Row 返回 null)。

ITenantIsolationContext(Scoped,跟随 ITenantContext 自动重算):当前请求的隔离上下文,暴露 Current / Mode / IsolationKey / ConnectionString。ITenantContext.Current 未初始化时抛 InvalidOperationException(继承 ADR-0004 fail-secure)。

DI 注册入口:

using Microsoft.Extensions.DependencyInjection;

services.AddMultiTenancyIsolationCore();
services.Configure<MultiTenancyIsolationOptions>(o =>
{
    o.DefaultSchemaTemplate = "uc_{id:00000000}";
    o.DatabaseConnectionStringResolver = id => db.GetConnectionString(id);
});

22. Resilience 弹性管道

Core 提供 AddResilienceCore() 注册 NoOp 管道(编译时占位),Polly 实现由 Galosys.Foundation.Polly.Core 提供。

services.AddCore();             // 内置 AddResilienceCore()(NoOp 默认)
services.AddPollyResilience();  // Polly 实现覆盖 NoOp(需引用 Polly.Core)

// 使用命名管道
var pipeline = provider.GetRequiredService<IResiliencePipelineProvider>()
    .GetPipeline("my-service");

await pipeline.ExecuteAsync(async ct =>
{
    return await httpClient.GetAsync("api/data", ct);
});

支持策略: Retry(重试)、CircuitBreaker(熔断)、Timeout(超时)、Bulkhead(隔离)、RateLimiter(限流)、Fallback(降级)。

{
  "Resilience": {
    "Pipelines": {
      "my-service": {
        "Retry": { "MaxRetryAttempts": 3, "BackoffType": "Exponential" },
        "CircuitBreaker": { "FailureThreshold": 0.5, "SamplingDuration": "00:01:00" },
        "Timeout": { "TimeoutInterval": "00:00:10" }
      }
    }
  }
}
类 所在模块 说明
IResiliencePipeline Core 弹性管道接口
IResiliencePipelineBuilder Core 管道构建器(链式配置策略)
IResiliencePipelineProvider Core 命名管道提供者
NoOpResiliencePipeline Core 默认 NoOp 实现(占位)
PollyResiliencePipeline Polly.Core Polly 真实实现
PollyResiliencePipelineBuilderFactory Polly.Core 工厂模式解耦

模块职责分离:Core 只定义抽象 + NoOp;Polly 实现由 Galosys.Foundation.Polly.Core 提供。下游模块通过 AddPollyResilience() 显式启用。


22. State Machine 状态机

Microsoft.Extensions.StateMachine — 三泛型 IStateMachine<S, E, C> 通用状态机,支持 Builder DSL、拦截器、监听器、持久化。

// 注册:Cola 风格 —— .On(event) 即注册,.Perform(action) 可选
services.AddStateMachine<OrderState, OrderEvent, OrderContext>(builder =>
{
    // 无副作用时不必写 .Perform
    builder.ExternalTransition()
        .From(OrderState.Pending).To(OrderState.Confirmed).On(OrderEvent.Submit);

    // 需要副作用再补 .Perform / .PerformAsync
    builder.ExternalTransition()
        .From(OrderState.Confirmed).To(OrderState.Shipped).On(OrderEvent.Ship)
        .When(ctx => ctx.StockEnough)
        .Perform((from, to, evt, ctx) => { });

    // 多源同 Target
    builder.ExternalTransitions()
        .FromAmong(OrderState.Shipped, OrderState.Confirmed)
        .To(OrderState.Cancelled)
        .On(OrderEvent.Cancel);

    // 横切关注点:公共 API 注入,无需触碰内部字段
    builder.AddInterceptor(new AuditInterceptor());
    builder.AddListener(new AuditLogger());
}, "order.orderStateMachine");   // machineId 约定:{module}.{machine}

// 从工厂取实例(工厂惰性从 DI 解析,无需额外 Build 回填)
var factory = provider.GetRequiredService<IStateMachineFactory>();
var machine = factory.Get<OrderState, OrderEvent, OrderContext>("order.orderStateMachine");

// 触发转换(需指定源状态,返回 FireResult)
var result = await machine.FireAsync(OrderState.Pending, OrderEvent.Submit, context);
// result.Status: Accepted / NoTransition / Rejected / Error
// result.SourceState / result.TargetState
// 失败时 result.Message 含 [machine='...']、已知 source states 或 permitted events、修复提示

// 可视化(入口状态标记 [*] --> X;内部转换 A -[A]-> A : E)
var uml = machine.GeneratePlantUml();
类 说明
IStateMachine<S,E,C> 状态机接口
IStateMachineFactory 状态机工厂
IStateMachineInterceptor 状态转换拦截器
IStateMachineListener 状态转换监听器
IStateMachinePersister 状态持久化(默认内存)
GeneratePlantUml() 输出 PlantUML(入口状态 [*] --> X、外部 A --> B : E1、内部 A -[A]-> A : E1)

BREAKING(仅影响自定义实现者):IStateMachineBuilder<S,E,C> 新增 ExternalTransitions() / AddInterceptor() / AddListener() 三个成员。使用内置 AddStateMachine 的业务代码无需改动。


23. Rules Engine 规则引擎

Microsoft.Extensions.Rules — 规则定义、构建、执行、事件监听与回退链。

// 注册
services.AddRules();

// 定义规则
[Rule(Name = "VipDiscount", Priority = 1)]
public class VipDiscountRule : IRuleEntity
{
    public RuleExecuteResult Execute(RuleDefinition rule, object context)
    {
        var order = (OrderContext)context;
        if (order.UserLevel == "VIP")
            return RuleExecuteResult.Success(new { Discount = 0.9 });
        return RuleExecuteResult.Skip();
    }
}

// 执行
var engine = provider.GetRequiredService<IRuleEngine>();
var result = await engine.ExecuteAsync("order.discount", orderContext);
类 说明
IRuleEngine 规则引擎接口
RuleBuilder 规则构建器
RuleDefinition 规则定义
FallbackChain 参数回退链
IRuleEventListener 规则事件监听器

24. Distributed Lock 分布式锁

Microsoft.Extensions.Locking.Distributed — 分布式锁抽象,内置内存实现。

// 注册
services.AddDistributedLock();

// 使用
var handle = await distributedLock.TryAcquireAsync("lock:order:123");
if (handle != null)
{
    try { /* 临界区 */ }
    finally { await handle.ReleaseAsync(); }
}
类 说明
IDistributedLock 分布式锁接口
IDistributedLockHandle 锁句柄
InMemoryDistributedLock 内存实现(用于开发和测试)

25. Distributed Idempotence 分布式幂等

Microsoft.Extensions.Idempotence — 分布式幂等性控制。

// 注册
services.AddIdempotence();

// 使用
var result = await idempotence.TryExecuteAsync("order:submit:123", async () =>
{
    return await orderService.SubmitAsync(order);
});
类 说明
IDistributedIdempotence 幂等性控制接口
InMemoryDistributedIdempotence 内存实现

26. Alarming 告警

Microsoft.Extensions.Alarming — 告警日志基础设施。

// 注册
services.AddAlarmer();

// 使用
alarmer.Alarm("数据库连接超时", new { Db = "OrderDb", Timeout = 30 });
类 说明
IAlarmer 告警器接口
Alarmer 告警器抽象基类
AlarmLogger 告警日志实现

27. AI 集成

Microsoft.Extensions.AI — 配置驱动的 AI 客户端 + RAG 能力。

27.1 注册

// 从 appsettings.json 读取 AI:Chat / AI:Embedding 配置
services.AddAI(configuration);

27.2 配置文件

"AI": {
  "Chat": {
    "Qwen":       { "Provider": "OpenAI",    "Endpoint": "", "ApiKey": "", "ModelId": "" },
    "DeepSeek":   { "Provider": "Anthropic",  "Endpoint": "", "ApiKey": "", "ModelId": "" },
    "Local":      { "Provider": "Ollama",     "Endpoint": "", "ApiKey": null, "ModelId": "" }
  },
  "Embedding": {
    "Qwen":       { "Provider": "Qwen",       "Endpoint": "", "ApiKey": "", "ModelId": "" },
    "OpenAI":     { "Provider": "OpenAI",     "Endpoint": "", "ApiKey": "", "ModelId": "" }
  }
}

27.3 Chat 调用

// 通过 key 获取对应 ChatClient
var qwen = services.GetRequiredKeyedService<IChatClient>("Qwen");

// 每次调用可设置不同 SystemPrompt
var response = await qwen.GetResponseAsync(
    [new(ChatRole.System, "你现在是一个英语翻译助手"),
     new(ChatRole.User, "你好")]);

27.4 Embedding 调用

var gen = services.GetRequiredKeyedService<IEmbeddingGenerator<string, Embedding<float>>>("Qwen");
var embeddings = await gen.GenerateAsync(["文本1", "文本2"]);

27.5 RAG(检索增强生成)

使用 UseRag() 前需先注册 VectorStore 实现(如 InMemory、Qdrant 等):

// 注册 VectorStore(自行选择实现)
services.AddSingleton<VectorStore>(sp => new MyVectorStore(...));

// 通过管道注册全局 RAG
services.AddChatClient(builder => builder
    .UseRag(configure: o => { o.Mode = RagMode.Simple; o.TopK = 3; })
    .Use(new OpenAIClient(apiKey)).AsChatClient("gpt-4"));

// 调用时动态控制
var response = await chatClient.GetResponseAsync(
    [new(ChatRole.User, "公司的年度目标是什么?")],
    new ChatOptions
    {
        AdditionalProperties = new()
        {
            ["rag_collection"] = "company_docs"
        }
    });

27.6 使用支持的 Chat Client

Provider 说明 实现方式
OpenAI(Chat+Embedding) OpenAI 兼容 API(含 Qwen/Azure) meai.openai 官方包
Anthropic(Chat) Anthropic Claude AnthropicClient SDK 包装
Ollama(Chat,扩展包) 本地 Ollama OllamaSharp
Qwen(Embedding) 通义千问 Embedding 内置 QwenEmbeddingGenerator

27.7 Chat 层中间件(护栏 / 压缩)

AddAI 为每个配置创建的 IChatClient 自动装配默认中间件管线,顺序为 UseOpenTelemetry → UseLogging → UseDistributedCache → UseCompaction → UseOutputGuardrail → [可选的 UseFunctionInvocation]。缺 DI 依赖(ILoggerFactory / IDistributedCache / 护栏 / 压缩服务)的中间件自动跳过。

开关位于 AIProviderOptions(Enable*,逐 provider 生效),默认值:EnableOpenTelemetry=true、EnableLogging=true、EnableDistributedCache=false(需应用注册 IDistributedCache)、EnableFunctionInvocation=false(防与 MAF agent 工具循环冲突)、EnableCompaction=true、EnableOutputGuardrail=true。

"AI": {
  "Chat": {
    "Qwen": { "Provider": "OpenAI", "Endpoint": "", "ApiKey": "", "ModelId": "",
              "EnableDistributedCache": true }
  }
}

也可在独立 IChatClient 上显式装配中间件(护栏/压缩原语位于 Microsoft.Extensions.AI 命名空间):

// 护栏:注册评估器链(TryAdd 语义,可覆盖)
services.AddOutputGuardrail(o => o.BlockedKeywords = ["机密"]);
// 压缩:注册选项
services.AddCompaction(o => o.MaxMessages = 32);

// 在 ChatClientBuilder 上显式挂载
services.AddChatClient(builder => builder
    .UseCompaction(o => o.Strategy = CompactionStrategy.Truncate)
    .UseOutputGuardrail(sp)
    .Use(new OpenAIClient(apiKey)).AsChatClient("gpt-4"));
类型 说明
OutputGuardrailChatClient / UseOutputGuardrail 输出护栏中间件,命中敏感词抛 OutputGuardrailException
CompactionChatClient / UseCompaction / CompactionOptions 对话压缩中间件,超限截断或摘要
AddOutputGuardrail / AddCompaction 护栏 / 压缩 DI 注册
IOutputGuardrailEvaluator、RuleBasedOutputGuardrail、IPiiDetector、IPiiRedactor 等 护栏/PII 原语(自 Galosys.Foundation.Agents.AI 下沉,无 MAF 依赖)

28. Excel 操作

IXlsHelper — 流式读取、流式写入、模板填充,支持列映射和自定义转换。

// 注册
services.AddXlsHelper();

// 流式读取
var records = xlsHelper.ExtractAsync<OrderDto>(stream, options);

// 流式写入
await xlsHelper.WriteAsync(stream, orders, options);

// 模板填充
await xlsHelper.FillAsync(templateStream, outputStream, data, options);
类 说明
IXlsHelper Excel 操作接口
IXlsWriter<T> 流式写入器
IXlsSheetWriter<T> 工作表写入器
XlsColumnAttribute 列映射特性
XlsSheetAttribute 工作表映射特性
AbstractXlsAppService Excel 应用服务基类

29. Responsibility Chain 责任链

public class ValidationHandler : AbstractChainHandler<OrderContext>
{
    public override async Task HandleAsync(OrderContext context, Func<Task> next)
    {
        if (context.IsValid)
            await next();
    }
}

// 执行
await chainClient.ExecuteAsync(context);

30. 数据结构

BloomFilter

var filter = new BloomFilter<string>(expectedItems: 100000, falsePositiveRate: 0.01);
filter.Add("item1");
bool exists = filter.Contains("item1"); // true

ConcurrentRunner — Pod 内并发运行器

启动 N 个 worker 并发执行同一个 task,提供 worker 级健康监控。适合 MQ 消费者 / 消息处理服务 / 后台 worker 场景。

⚠️ 弹性伸缩由 K8s HPA / 进程管理器在 Pod 副本层面完成。本抽象不参与 Pod 内 worker 数动态调整。

用法
// 1. 创建运行器(worker 数固定,由 Deployment 配置决定)
var runner = ConcurrentRunner.CreateTaskRunner(workerCount: 8);

// 2. 启动 — task 形态不限:长循环 / 单次都允许
runner.Start(async ct =>
{
    while (!ct.IsCancellationRequested)
    {
        heartbeat.Pulse();                              // ← 业务侧主动脉冲心跳
        var msg = await queue.PullAsync(ct);             // 长循环 pull 模型
        await ProcessMessage(msg, ct);
    }
});

// 3. 优雅停止(配合 K8s terminationGracePeriod,建议 < gracePeriod)
await runner.StopAsync(TimeSpan.FromSeconds(25));       // 超时即返回
await runner.DisposeAsync();                            // 幂等

// 4. 监控 Slots(接入 ASP.NET Core HealthChecks / Prometheus)
var slots = runner.Slots;                               // 快照
var stuckCount = slots.Count(s =>
    s.LastHeartbeatAt.HasValue &&
    DateTimeOffset.UtcNow - s.LastHeartbeatAt > TimeSpan.FromMinutes(5));
接口签名
public interface IConcurrentRunner : ILoggerAccessor, IAsyncDisposable
{
    int WorkerCount { get; }                              // 配置值(固定)
    int CurrentWorkerCount { get; }                       // 当前活跃
    bool IsRunning { get; }
    IReadOnlyList<RunnerSlotState> Slots { get; }         // worker 状态快照

    void Start(Func<CancellationToken, Task> task);
    ValueTask StopAsync(CancellationToken ct = default);  // 默认 30s 兜底 deadline
    ValueTask StopAsync(TimeSpan timeout);                // 自定义截止时间
}

public sealed record RunnerSlotState(
    int Index,
    int? ManagedThreadId,
    RunnerSlotStatus Status,                              // Starting / Running / Draining / Stopped / Faulted
    long ProcessedCount,
    DateTimeOffset StartedAt,
    DateTimeOffset? LastHeartbeatAt);                     // 接入健康检查

public static class ConcurrentRunner
{
    public static IConcurrentRunner CreateTaskRunner(int workerCount);  // 1-1024
}
命名空间

寄生 System.Threading.Tasks,符合"命名空间寄生"约定。

跟 Cola IExecutor<TCommand> 区分
类型 职责
IExecutor<TCommand> 处理单个特定命令(CQRS 风格,命令分发)
IConcurrentRunner 运行 N 个并发 task(消费者池风格,长跑 worker)

两者职责独立,命名空间互不污染,不互相依赖。

健康检查接入
services.AddHealthChecks()
    .AddCheck<IConcurrentRunner>("concurrent_runner_workers", check: (runner, ct) =>
    {
        var stuck = runner.Slots.Count(s =>
            s.LastHeartbeatAt.HasValue &&
            DateTimeOffset.UtcNow - s.LastHeartbeatAt > TimeSpan.FromMinutes(5));
        return stuck == 0
            ? HealthCheckResult.Healthy()
            : HealthCheckResult.Unhealthy($"{stuck} workers stuck");
    });

K8s livenessProbe 会调健康检查端点,卡住的 worker → Pod unhealthy → K8s 自动重启 Pod。

WorkerPool

// 注册
services.AddWorkerPool();

// 使用
await workerPool.EnqueueAsync(async ct => { /* 工作任务 */ }, cancellationToken);
实现 说明
TaskWorkerPool 基于 Task 的工作池
ThreadWorkerPool 基于 Thread 的工作池

ChannelTemplate

基于 Channel<T> 的消息管道模板,实例级 + 可配置。同一 T 类型每个实例独立一份 channel;不同类型实例互不干扰。

// 方式一:双参数构造(推荐,按 ChannelTemplateOptions 配置)
var template = new ChannelTemplate<RequestLog>(
    logger,
    Options.Create(new ChannelTemplateOptions
    {
        Capacity = 50_000,
        FullMode = BoundedChannelFullMode.DropOldest,
        WriteFilter = item => ShouldSample(item)                  // 可选采样
    }));
await template.WriteAsync(new RequestLog(...));                  // 应用 WriteFilter
template.TryWrite(item);                                   // 不经过 WriteFilter(fire-and-forget)

// 方式二:旧单参数构造(向后兼容,使用默认 options)
var legacy = new ChannelTemplate<RequestLog>(logger);

// 批量读取(最多 200 条 / 500ms 超时返回当前批次)
await foreach (var batch in template.ReadBatchAsync(200, TimeSpan.FromMilliseconds(500), ct))
{
    await ProcessAsync(batch);
}

// 逐条读取(失败自动重写回 channel)
await template.ReadAsync(async msg => await ProcessAsync(msg));

ChannelTemplateOptions(System.Threading.Channels 命名空间):

属性 类型 默认 说明
Capacity int 10_000 底层 BoundedChannel<T> 容量
FullMode BoundedChannelFullMode DropWrite 满载策略:Wait / DropOldest / DropWrite / DropNewest
SingleReader bool false 单消费者优化标记
SingleWriter bool false 单生产者优化标记
WriteFilter Func<object, bool>? null 采样过滤器;返回 false 时 WriteAsync 静默丢弃(TryWrite 不经过 filter)

ReadBatchAsync(maxBatch, interval, ct) 语义:

  • 累积到 maxBatch 条立即 yield
  • 未满则等 interval 后 yield 当前批次
  • ct 触发时退出循环
  • 空 channel 不 yield 空批次(继续等待下一轮)

ApplicationMessagePublisher — 泛型路由 + 批量消费

按消息类型路由到独立 ChannelTemplate<T>,支持事件监听器(IApplicationListener<T>)与批量消费者(RegisterBatchConsumer<T>)。

// 发布(按 T 路由到对应 ChannelTemplate<T>)
var publisher = sp.GetRequiredService<ApplicationMessagePublisher>();
await publisher.PublishAsync(new OrderPlacedEvent(orderId));

// 注册批量消费者(统一由 ApplicationMessagePublisherConsumer 后台服务驱动)
services.Configure<ApplicationMessagePublisherOptions>(o =>
    o.RegisterBatchConsumer<RequestLogCreateInput>(
        handler: async batch => await SaveRequestLogsAsync(batch),
        batchSize: 200,
        interval: TimeSpan.FromMilliseconds(500)));

// 注册事件监听器(自动扫描 IApplicationListener<T>,由 Bootstrapper 启动)
public sealed class OrderPlacedListener : IApplicationListener<OrderPlacedEvent>
{
    public Task<bool> HandleAsync(OrderPlacedEvent e) { /* ... */ return Task.FromResult(true); }
}

监听器读取循环在两种时机启动,且每个消息类型只启动一次:

  • Host 启动:RegisterListenersAsync() 遍历已注册的监听器消息类型,物化 channel 并挂载读取循环;
  • 首次发布:GetChannel<T>() 创建 channel 时若发现监听器,立即挂载读取循环。

互斥约束:同一消息类型只能选择一种消费模型。若同时注册 IApplicationListener<T> 与批量消费者, 框架会在 Host 启动时抛出 InvalidOperationException(RegisterBatchConsumer<T> 调用时也会立即抛出), 避免两条读取循环竞争同一队列。

AddApplicationMessagePublisher() 自动注册:

  • ChannelTemplate<>(开泛型)
  • ApplicationMessagePublisher(DI 单例)
  • ApplicationMessagePublisherBootstrapper(启动 listener 循环)
  • ApplicationMessagePublisherConsumer(启动批量消费者循环)
  • ChannelTemplateOptions 默认配置

31. 工具类

Env — 环境检测

Env.IsKubernetes;        // 是否在 K8s 中运行
Env.MachineIP;           // 本机 IP 地址
Env.MachineName;         // 主机名
Env.IsDevelopment();     // 是否开发环境

ServiceLocator — 服务定位器

var service = ServiceLocator.GetService<IMyService>();

Poco — DTO 基类

public class UserDto : Poco
{
    public string Name { get; set; }
}
var clone = original.Clone();

ITreeNode — 树形结构

var tree = TreeNodeHelper.BuildTree(nodes);
var flat = TreeNodeHelper.Flatten(tree);

ITimeProvider — 时间提供者

public class MyService
{
    public MyService(ITimeProvider time) { }
    // 测试时可注入 mock 时间
}

数据脱敏

public class UserDto
{
    [DataMask(MaskType.Partial)]
    public string Phone { get; set; } // 138****1234
}

API 签名

var signature = SignUtils.GenerateSign(parameters, secretKey);
var valid = SignUtils.VerifySign(parameters, secretKey, signature);

二倍均值算法(红包分配)

var amounts = Bma.Divide(totalAmount: 100, count: 5);

命名帮助

var queue = Naming.Queue("order");     // 队列命名
var topic = Naming.Topic("order");     // 主题命名
var key = Naming.Key("order:123");     // Key 命名

Enumeration — 智能枚举

public class OrderStatus : Enumeration<OrderStatus>
{
    public static readonly OrderStatus Pending = new(1, "待处理");
    public static readonly OrderStatus Completed = new(2, "已完成");
}

32. DI 服务注册

// 核心注册(模块化、消息总线、消息发布等)
// 幂等:模块路径(UseModularization)与显式路径共存时不重复注册
services.AddCore();

// COLA 架构
services.AddCola(typeof(MyCommand).Assembly);

// CQRS Lite(一行扫描所有 ICommandHandler<,> 注册 + Dispatcher Singleton)
// 零参默认走 DependencyContext.Default.GetImplementTypes()(与框架其余扫描器一致:
// 拓扑排序后从 AppContext.BaseDirectory 加载运行时 DLL,自动排除测试 SDK 程序集并全局缓存)
services.AddCqrs();
// 或显式传程序集(xUnit test host / AOT / 限定扫描范围场景推荐)
services.AddCqrs(typeof(MyCommand).Assembly);

// 消息总线 + Outbox
services.AddMessageBus();
services.AddOutbox();

// 弹性管道(NoOp 默认)
services.AddResilienceCore();
// 若需 Polly 实现:services.AddPollyResilience();

// 状态机
services.AddStateMachine();

// 规则引擎
services.AddRules();

// 分布式锁
services.AddDistributedLock();

// 分布式幂等
services.AddIdempotence();

// 告警
services.AddAlarmer();

// Excel 操作
services.AddXlsHelper();

// AI 集成(读取 appsettings.json 的 AI 节)
services.AddAI(configuration);

// 服务发现
services.AddServiceDiscovery();

// 健康检查(自动发现 [HealthCheck] 属性探针 + 内置 ResourceUtilization/Lifecycle)
services.AddHealthChecksAll();

// 自动扫描注册
// 排除依赖测试 SDK(xunit/NUnit/MSTest 等)的程序集,宿主与集成包照常扫描
services.AddScannable();

32.1 组件默认生命周期(DefaultLifetime)

ComponentAttribute 提供虚属性 DefaultLifetime,子类可覆写声明该特性标注的组件默认注册到 DI 时的生命周期。AddAnnotationConfiguration 扫描时以 DefaultLifetime 作为生命周期初值,仍可被显式标注 [Singleton] / [Scoped] / [Transient] 覆盖。

特性 默认生命周期 说明
[Service] Transient 不重写
[Repository] Scoped 重写为 ServiceLifetime.Scoped(依赖 Scoped DbContext)
[Handler] Transient 不重写
[Query] Transient 不重写
[ExceptionHandler] Singleton 重写为 ServiceLifetime.Singleton(处理器无状态,按异常类型分发;定义于 Core System 命名空间)

自定义组件特性也可继承 ComponentAttribute 并覆写 DefaultLifetime,由框架统一处理。

32.2 模块化异常处理(ITypedExceptionHandler)

Core 在命名空间 System 内统一提供模块化异常处理能力:ITypedExceptionHandler / ITypedExceptionHandler<TException> 抽象入口、TypedExceptionHandler<TException> 强类型基类、ExceptionHandlingOutput 纯数据输出模型、ExceptionHandlingOptions + ExceptionResponseFormat 响应格式选项、[ExceptionHandler] 自动注册特性、DefaultExceptionHandler 兜底处理器,以及 IExceptionPipeline / ExceptionPipeline / ExceptionHandlingResult 统一分发管道。所有接口签名均不依赖 HttpContext,Console / Wpf / Web 任意宿主均可直接消费。

定义一个处理器只需继承 TypedExceptionHandler<TException> 并标注 [ExceptionHandler]:

[ExceptionHandler(Priority = 100)]
public sealed class OrderBizExceptionHandler : TypedExceptionHandler<OrderBizException>
{
    public override ValueTask<ExceptionHandlingOutput> HandleAsync(
        OrderBizException exception, CancellationToken ct = default)
        => ValueTask.FromResult(new ExceptionHandlingOutput(400, exception.ErrCode, exception.ErrMessage));
}

Core 内置四个框架默认处理器 ArgumentExceptionHandler / DbExceptionHandler / AbstractExceptionHandler / ValidationExceptionHandler(均 [ExceptionHandler(0)]),按既有 catch 分支产出统一错误码(A00400 参数/数据库/Validation、透传 AbstractException.ErrCode);DefaultExceptionHandler([ExceptionHandler(int.MinValue)])作为纯兜底,仅处理未知异常产出 500 B00500。

Core 同时提供 IExceptionPipeline(Process(exception, traceId?, source?)),统一「立即 LogError → 按 Priority 降序分发 → 处理器异常降级 → 兜底 500 B00500」;AspNetCore 中间件与 Wpf binder 委托该管道。详见 AspNetCore README §模块化异常处理 与 Wpf README §异常处理。


33. 目录结构

Galosys.Foundation.Core/
├── Galosys\Foundation\Core\
│   ├── ModuleDescriptor.cs
│   ├── Executor/                  # COLA Executor 模板方法
│   ├── ExtensionPoint/            # COLA ExtensionPoint 扩展点
│   └── Xls/                       # Excel 操作
├── Microsoft\Extensions\
│   ├── AI/                        # AI 集成(通义千问)
│   ├── Alarming/                  # 告警基础设施
│   ├── Configuration/             # 配置绑定扩展
│   ├── DependencyInjection/       # DI 注册 + 模块化系统
│   ├── Diagnostics/               # 诊断 + Metrics
│   ├── Hosting/                   # HostBuilder 扩展
│   ├── Idempotence/               # 分布式幂等
│   ├── Locking/Distributed/       # 分布式锁
│   ├── Messaging/                  # CQRS Lite Behaviors(Phase 2+ 占位;契约已迁入 Galosys/Foundation/Core/)
│   ├── MessageBus/                # 消息总线 + Outbox 引擎 (renamed from Messaging)
│   ├── Options/                   # 配置选项类
│   ├── Resilience/                # 弹性管道
│   ├── Rules/                     # 规则引擎
│   ├── StateMachine/              # 状态机
│   └── MessageBus/                # 消息总线
├── System\
│   ├── BloomFilter.cs             # 布隆过滤器
│   ├── ChannelTemplate.cs         # Channel 消息管道模板
│   ├── DateTimeExtensions.cs
│   ├── Env.cs                     # 环境检测
│   ├── Enumeration.cs             # 智能枚举
│   ├── Naming.cs                  # 命名帮助
│   ├── Net/WebSockets/Client/     # WebSocket Layer 1(ExponentialBackoff / ArrayPoolWebSocketBuffer / WebSocketMessageTypeExtensions)
│   ├── Poco.cs                    # DTO 基类
│   ├── ServiceLocator.cs          # 服务定位器
│   ├── Security/Cryptography/     # 加密算法
│   ├── StringExtensions.cs
│   └── WorkerPool/                # 工作池
└── System\Text\Json\Serialization\
    └── DataMaskConverter.cs       # 数据脱敏

34. 测试

Outbox 测试覆盖

测试文件 组件 用例数
InMemoryOutboxStoreTests InMemoryOutboxStore 14
OutboxRelayTests OutboxRelay 后台轮询 6
MessageBusOutboxTests MessageBus outbox 路径 4
PendingMsgColTests PendingMsgCol AsyncLocal 收集器 3
# 运行 outbox 存储层测试
dotnet test framework/test/Galosys.Foundation.Core.Tests/ --filter "FullyQualifiedName~InMemoryOutboxStoreTests"

# 运行 outbox 服务层测试
dotnet test framework/test/Galosys.Foundation.Core.Tests/ --filter "FullyQualifiedName~OutboxRelay"
dotnet test framework/test/Galosys.Foundation.Core.Tests/ --filter "FullyQualifiedName~MessageBusOutbox"

35. 依赖

  • Microsoft.Extensions.Caching.Abstractions / Memory
  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.DependencyModel
  • Microsoft.Extensions.Diagnostics.HealthChecks
  • Microsoft.Extensions.Hosting
  • Microsoft.Extensions.Http
  • Microsoft.Extensions.ObjectPool
  • Polly.Core
  • System.Threading.RateLimiting

Outbox 引擎零新增依赖,复用已有 Microsoft.Extensions.Hosting / DI。


36. 时间抽象迁移

新代码统一使用 BCL System.TimeProvider(.NET 10 内置时间抽象),AddCore() 已注册 services.TryAddSingleton(TimeProvider.System):

// DI 注入(新代码首选)
public class OrderService(TimeProvider clock)
{
    public DateTimeOffset Now => clock.GetUtcNow();
}

// 测试:注入 FakeTimeProvider 精确控制时间
var fixture = new TimeProviderFixture();     // Galosys.Foundation.Testing
services.AddSingleton<TimeProvider>(fixture.TimeProvider);

自研 ITimeProvider / SystemTimeProvider 已标记 [Obsolete("Use System.TimeProvider directly. Will be removed in vNext.", false)]:

  • 存量代码继续使用 ITimeProvider 仅产生 CS0618 警告,构建不受阻(TreatWarningsAsErrors=false)
  • 迁移替代:接口仅 DateTimeOffset Now 一个成员 → BCL 等价 TimeProvider.GetUtcNow()
  • 预计 vNext 移除接口;SystemTimeProvider 暂保留为 DI 过渡解析目标

37. 有序 GUID 生成

有序 GUID(时间排序 + 唯一性)直接使用 BCL Guid.CreateVersion7()(.NET 9+,V7 UUID,排序顺序即生成顺序):

var id = Guid.CreateVersion7();   // 推荐用法

LocalGenerator 已标记 [Obsolete("Use Guid.CreateVersion7() directly. Will be removed in vNext.", false)]:

  • LocalGenerator.NextGuid() → 等价 Guid.CreateVersion7()(新增,V7 时间排序)
  • LocalGenerator.CombGuid() / SeqGuid() 内部已改用 V7 字节,输出行为不变
  • 其余 ID 策略(Snowflake / Ulid / Nanoid / Sqids / NextId)不受影响

38. LogProperties 与 Telemetry 生成器

Core 引用了 Microsoft.Extensions.Telemetry.Abstractions,其 [LoggerMessage] 源生成器替换 BCL 内置实现(Microsoft.Gen.Logging,v10.1.0;命名空间仍为 Microsoft.Extensions.Logging):

// [LogProperties] 将强类型 record 展开为结构化字段
[LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "Trace: {Temperature}")]
internal static partial void LogTemperature(ILogger logger, [LogProperties] Temperature temperature);
  • 日志字段以 {参数名}.{属性} 展开(如 temperature.Value、temperature.Unit),可从日志 state 读取
  • 性能敏感路径继续使用 [LoggerMessage]([LogProperties] 引入反射/序列化开销)
  • 存量 [LoggerMessage] 调用无需改动,生成器自动接管

39. WebSocket Layer 1 — 原子能力(System.Net.WebSockets.Client)

面向客户端的 WebSocket 零依赖原语层,寄生在 System.Net.WebSockets.Client 命名空间,业务侧 using System.Net.WebSockets; 后即可同时获得 ClientWebSocket + 扩展方法。本层仅原子能力,完整的连接生命周期、心跳、自动重连、DI 注册、生命周期集成等高层特性请使用后续的独立模块 Galosys.Foundation.WebSockets.Client(规划中)。

39.1 ExponentialBackoff — 指数退避 + 抖动

using System.Net.WebSockets.Client;

// 第 0 次重试:1s ± 20% 抖动
var d0 = ExponentialBackoff.NextDelay(0, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30));
// 第 5 次重试:封顶 30s
var d5 = ExponentialBackoff.NextDelay(5, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30));

// 生成完整延迟序列(jitterRatio=0 时严格指数增长,便于测试断言)
var seq = ExponentialBackoff.Sequence(maxAttempts: 5, baseDelay: TimeSpan.FromSeconds(1),
    maxDelay: TimeSpan.FromSeconds(30), jitterRatio: 0);
foreach (var (delay, attempt) in seq.Select((d, i) => (d, i)))
    Console.WriteLine($"attempt={attempt} delay={delay.TotalSeconds}s");

// 注入确定性 Random 做单测
var rng = new Random(seed: 42);
var deterministic = ExponentialBackoff.NextDelay(0, TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(30), jitterRatio: 0.2, random: rng);

39.2 ArrayPoolWebSocketBuffer — 基于 ArrayPool 的可重用接收缓冲

struct : IDisposable(可变结构体)。从 ArrayPool<byte>.Shared 借用缓冲,Dispose 时归还。

using var buffer = new ArrayPoolWebSocketBuffer();         // 默认 4096 字节
using var large = new ArrayPoolWebSocketBuffer(minSize: 65536);  // 自定义大小

buffer.Advance(100);
Console.WriteLine($"已写入 {buffer.Written} 字节");

buffer.Reset();   // 复用同一缓冲,仅清零 Written
// ... 重新接收 ...

buffer.Dispose(); // 归还到 ArrayPool;之后 Memory 返回 Memory<byte>.Empty

边界:构造时 minSize < 4096 自动提升到 4096,对齐 WebSocket 单帧最大载荷语义。Advance(-1) 抛 ArgumentOutOfRangeException。

39.3 WebSocketMessageTypeExtensions — 帧类型判断与本地化描述

using System.Net.WebSockets.Client;

var t = WebSocketMessageType.Text;
Console.WriteLine(t.IsControl());              // False
Console.WriteLine(t.IsData());                 // True
Console.WriteLine(t.ToChineseDescription());   // "文本"

var status = WebSocketCloseStatus.NormalClosure;
Console.WriteLine(status.ToChineseDescription());   // "正常关闭"

// 未知值兜底
Console.WriteLine(((WebSocketCloseStatus)9999).ToChineseDescription()); // ""

.NET 10 WebSocketMessageType 仅暴露 Text / Binary / Close(Ping/Pong 由 ClientWebSocket.ReceiveAsync 内部处理),IsControl 等价于"是否为关闭帧"。

39.4 Layer 1 使用边界声明

用法 推荐层级
仅需发送/接收一次 WebSocket 消息(脚本、单元测试、调试工具) Layer 1(本节 API)
长连接 + 自动重连 + 心跳 + 多连接隔离 + 生命周期集成 + 配置绑定 Layer 2 独立模块 Galosys.Foundation.WebSockets.Client(已落地,见 §39.7)
WebSocket 服务端 ASP.NET Core 内置 WebSocketMiddleware(不在本仓库)

39.5 ClientWebSocketExtensions — 连接 + 发送便捷

using System.Net.WebSockets.Client;

// 指数退避重连(默认无限重试,BaseDelay 1s, MaxDelay 30s, JitterRatio 0.2)
using var ws = new ClientWebSocket();
await ws.ConnectWithRetryAsync(new Uri("wss://echo.example.com"));

// 发送便捷
await ws.SendTextAsync("hello");                                    // UTF-8 Text 帧
await ws.SendBinaryAsync(new byte[] { 1, 2, 3 });                   // Binary 帧
await ws.SendJsonAsync(new { Action = "ping", Ts = DateTime.UtcNow }); // JSON → Text 帧(默认 camelCase)
await ws.SendPingAsync(new byte[] { 0xAA, 0xBB });                  // 应用层 ping(当前以 Binary 帧实现,见 §39.5.1)

// 幂等关闭(重复调用安全;CloseSent/Aborted 状态直接返回)
await ws.EnsureClosedAsync(WebSocketCloseStatus.NormalClosure, "bye");
39.5.1 SendPingAsync 实现说明

.NET 10 ClientWebSocket 不暴露 发送真实 Ping 控制帧的公共 API(Ping/Pong 在帧层内部处理)。当前 SendPingAsync 回退为发送 Binary 帧,由应用层协议解释为"Ping 业务消息"。Layer 2 真实 Ping 帧走 WebSocket.KeepAliveInterval 传输层 keep-alive。

39.5.2 ConnectRetryOptions 自定义
var options = new ConnectRetryOptions
{
    MaxAttempts = 5,                              // 最多 5 次尝试
    BaseDelay = TimeSpan.FromMilliseconds(500),   // 基础延迟 500ms
    MaxDelay = TimeSpan.FromSeconds(5),           // 封顶 5 秒
    JitterRatio = 0,                              // 无抖动(测试可断言精确延迟)
};
await ws.ConnectWithRetryAsync(uri, options, ct);

39.6 WebSocketExtensions — 接收循环

using System.Net.WebSockets.Client;
using System.Buffers;

// ===== 1. 单帧接收(调用方负责 buffer Dispose) =====
using (var buffer = new ArrayPoolWebSocketBuffer())
{
    var result = await ws.ReceiveSingleFrameAsync(buffer, ct);
    if (result.MessageType == WebSocketMessageType.Text)
    {
        var text = Encoding.UTF8.GetString(buffer.Memory.Span.Slice(0, result.Count));
        Console.WriteLine($"recv: {text}");
    }
}

// ===== 2. 分片累积 + 完整消息回调(async 安全,holder 类必须) =====
using var slot = new ArrayPoolWebSocketBufferRef(new ArrayPoolWebSocketBuffer());
await ws.ReceiveAccumulatingAsync(slot, (type, payload) =>
{
    return type switch
    {
        WebSocketMessageType.Text => HandleTextAsync(Encoding.UTF8.GetString(payload.ToArray()), ct),
        WebSocketMessageType.Binary => HandleBinaryAsync(payload.ToArray(), ct),
        WebSocketMessageType.Close => HandleCloseAsync(ct),
        _ => ValueTask.CompletedTask
    };
}, ct);
// 循环退出后 slot.Dispose 已被 finally 调用;slot.Buffer.Memory 返回 Memory<byte>.Empty
39.6.1 为什么用 ArrayPoolWebSocketBufferRef 而非 ref ArrayPoolWebSocketBuffer

C# 异步方法不允许 ref / in / out 参数。由于 ArrayPoolWebSocketBuffer 是 struct,接收循环 finally 块中的 buffer.Dispose() 若直接接收 struct 只会清理副本。ArrayPoolWebSocketBufferRef 持有 buffer 字段(class 引用语义),让 finally 真正调用到调用方持有的 buffer。这是 .NET 中 IDisposable struct 跨 await 边界的标准做法。

39.7 端到端示例 — 回环 + JSON

// 集成测试场景:发送 JSON 业务消息并 echo 验证
using var ws = new ClientWebSocket();
await ws.ConnectWithRetryAsync(echoServerUri);

var request = new { Action = "subscribe", Topic = "order.created" };
await ws.SendJsonAsync(request);

using var slot = new ArrayPoolWebSocketBufferRef(new ArrayPoolWebSocketBuffer());
await ws.ReceiveAccumulatingAsync(slot, (type, payload) =>
{
    if (type == WebSocketMessageType.Text)
    {
        var response = JsonSerializer.Deserialize<EchoResponse>(payload.ToArray());
        Console.WriteLine($"echo: {response?.Action}");
    }
    return ValueTask.CompletedTask;
}, ct);

39.7 Layer 2 引用 — Galosys.Foundation.WebSockets.Client

Layer 1 仅提供原子能力(单次收发、缓冲、退避)。长连接、自动重连、心跳、多连接隔离、DI 注册、生命周期集成等高层特性请使用独立模块 Galosys.Foundation.WebSockets.Client(Phase 2 已落地):

能力 模块
单次 WebSocket 收发(脚本 / 测试 / 调试) Layer 1(§39.1–39.6)
长连接 + 自动重连 + 心跳 + 多连接 + DI + 生命周期 Galosys.Foundation.WebSockets.Client(此模块)
WebSocket 服务端 ASP.NET Core 内置 WebSocketMiddleware(不在本仓库)

模块提供 IWebSocketClient(单连接抽象)、IWebSocketClientManager(多连接管理)、IWebSocketHandler<T>(业务处理器)、IWebSocketMessageSerializer<T>(反序列化抽象)、WebSocketClientOptions(连接配置)、HeartbeatOptions(心跳配置,4 模式 phase2-2 落地)。使用示例见 Galosys.Foundation.WebSockets.Client README。

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (148)

Showing the top 5 NuGet packages that depend on Galosys.Foundation.Core:

Package Downloads
Galosys.Foundation.AspNetCore

Galosys.Foundation快速开发库

Galosys.Foundation.Data

Galosys.Foundation快速开发库

Galosys.Foundation.AspNetCore.DynamicApi

Galosys.Foundation快速开发库

Galosys.Foundation.Actuator

Galosys.Foundation快速开发库

Galosys.Foundation.HttpClient

Galosys.Foundation快速开发库

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
26.10.29.1 1,714 10/29/2026
26.10.20.1 1,418 10/20/2026
26.10.16.2 1,424 10/16/2026
26.10.16.1 1,406 10/16/2026
26.10.9.1 1,364 10/9/2026
26.10.8.1 1,414 10/8/2026
26.9.23.1 1,404 9/23/2026
26.9.16.1 1,436 9/16/2026
26.9.15.1 1,385 9/15/2026
26.9.14.1 1,422 9/14/2026
26.9.10.1 1,009 9/10/2026
26.9.3.1 912 9/3/2026
26.8.29.1 1,442 8/31/2026
26.8.26.1 1,511 8/26/2026
26.8.23.1 1,919 8/23/2026
26.8.21.1 2,063 8/21/2026
26.8.20.1 2,110 8/20/2026
26.8.18.1 1,662 8/18/2026
26.8.17.1 1,671 8/17/2026
26.8.13.2 1,429 8/13/2026
Loading failed