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
<PackageReference Include="Galosys.Foundation.Core" Version="26.10.29.1" />
<PackageVersion Include="Galosys.Foundation.Core" Version="26.10.29.1" />
<PackageReference Include="Galosys.Foundation.Core" />
paket add Galosys.Foundation.Core --version 26.10.29.1
#r "nuget: Galosys.Foundation.Core, 26.10.29.1"
#:package Galosys.Foundation.Core@26.10.29.1
#addin nuget:?package=Galosys.Foundation.Core&version=26.10.29.1
#tool nuget:?package=Galosys.Foundation.Core&version=26.10.29.1
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新 PagedRequestPageNumPageNoPageSizePageSize
旧 PageOutput新 PagedResult<T>Rows(IEnumerable)Data(IReadOnlyList<T>)TotalTotalRecordsPagesTotalPages(计算)HasNextHasNextPage(计算)HasPreviousHasPreviousPage(计算)
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必须唯一且建索引(典型:自增longId / 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 | Versions 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. |
-
net10.0
- Microsoft.Extensions.AI (>= 10.7.0)
- Microsoft.Extensions.AI.Abstractions (>= 10.7.0)
- Microsoft.Extensions.AI.OpenAI (>= 10.7.0)
- microsoft.extensions.caching.abstractions (>= 10.0.9)
- microsoft.extensions.caching.hybrid (>= 10.0.0)
- microsoft.extensions.caching.memory (>= 10.0.0)
- microsoft.extensions.compliance.redaction (>= 10.0.0)
- microsoft.extensions.dependencyinjection (>= 10.0.0)
- microsoft.extensions.dependencymodel (>= 10.0.0)
- microsoft.extensions.diagnostics.healthchecks (>= 10.0.10)
- microsoft.extensions.diagnostics.healthchecks.common (>= 10.0.0)
- microsoft.extensions.diagnostics.healthchecks.resourceutilization (>= 10.1.0)
- microsoft.extensions.hosting (>= 10.0.0)
- microsoft.extensions.http (>= 10.0.0)
- microsoft.extensions.objectpool (>= 10.0.1)
- microsoft.extensions.servicediscovery (>= 10.0.0)
- Microsoft.Extensions.Telemetry.Abstractions (>= 10.1.0)
- Microsoft.Extensions.Validation (>= 10.0.11)
- Microsoft.Extensions.VectorData.Abstractions (>= 10.7.0)
- system.threading.ratelimiting (>= 10.0.9)
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 |