SummerFrame.Uow 0.2.0

dotnet add package SummerFrame.Uow --version 0.2.0
                    
NuGet\Install-Package SummerFrame.Uow -Version 0.2.0
                    
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="SummerFrame.Uow" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SummerFrame.Uow" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="SummerFrame.Uow" />
                    
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 SummerFrame.Uow --version 0.2.0
                    
#r "nuget: SummerFrame.Uow, 0.2.0"
                    
#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 SummerFrame.Uow@0.2.0
                    
#: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=SummerFrame.Uow&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=SummerFrame.Uow&version=0.2.0
                    
Install as a Cake Tool

Summer.Frame

仓库地址:https://github.com/liaoyuanxiaohuo/Summer.Frame

一个精简的 .NET 框架:DDD 领域模型、EF Core + Dapper 双仓储(同一 UoW 共享事务)、工作单元(UoW)、分布式缓存(Redis)、分布式锁(Redis 自实现)、本地/分布式事件总线(RabbitMQ)、全文搜索(Elasticsearch)、健康检查与 OpenTelemetry 可观测性。库多目标 net8.0;net9.0;net10.0,不引入模块系统,全部用原生 IServiceCollection 扩展方法(AddSummerXxx)组装。

能力清单

  • DDD 领域模型Entity<TKey> / AggregateRoot<TKey>,聚合根内收集本地事件(AddLocalEvent)与分布式事件(AddDistributedEvent),乐观并发戳 ConcurrencyStampSummerDbContext 自动维护。
  • 仓储(EF Core + Dapper 双实现,同一 UoW 共享事务)IReadOnlyRepository / IRepository 提供 provider 无关的丰富方法(按主键/按条件 Get/Find/GetList/GetCount/Any、分页 + 排序、批量增改删);EF 专属能力(Include/AsNoTracking、软删除、库内批量 Update/Delete)在 IEfCoreRepositoryIDbConnectionProvider 让 Dapper 复用 DbContextProvider 为当前 UoW 开启的同一个数据库连接与事务,EF 写 + Dapper 写天然共提交/共回滚。仓储可按 DbSet 与程序集约定自动注册。详见 仓储用法
  • 工作单元(UoW)IUnitOfWorkManager.Begin() 开启环境 UoW;ITransactionApi/ISupportsSavingChanges 统一各类事务资源的 SaveChanges → Commit/Rollback;完成顺序:SaveChangesAsync → 发布本地事件 → 底层事务 Commit → 发布分布式事件。
  • 分布式缓存(Redis)IDistributedCache<TCacheItem>.GetOrAddAsync,基于 StackExchange.Redis,key 统一加前缀 + 类型名隔离,值走 Json/Protobuf 序列化。
  • 分布式锁(Redis 自实现)IDistributedLock.TryAcquireAsyncSET NX PX 抢锁 + Lua 脚本保证释放的原子性(校验持有者 token)+ 看门狗自动续期,避免长任务持锁期间因过期被误释放。
  • EventBus(本地 + 分布式):所有事件 DTO 实现标记接口 IEventILocalEventHandler<T> / IDistributedEventHandler<T> 按程序集扫描注册;IUnitOfWork 缓冲聚合根产生的事件,UoW 完成时统一发布。
  • 分布式 EventBus(RabbitMQ)特性驱动——事件 DTO 上用 [DistributedEvent] 配置交换机名/类型(direct/topic/fanout)/routingKey/序列化(Json 或 Protobuf),Handler 上用 [EventHandler] 配置队列名;AddSummerRabbitMq(configure, assemblies) 扫描 Handler 自动建订阅,无需逐个事件手动注册。生产可靠性:QoS 预取、消息持久化、失败退避重试 + 死信队列(不丢弃)、按 MessageId 幂等去重、断线自动重连(拓扑恢复 + 启动重试)。详见 EventBus 用法
  • 全文搜索(Elasticsearch)ISearchService(provider 无关:按类型分索引,Index/Delete/Search),实现 Summer.Search.Elasticsearch(官方 Elastic.Clients.Elasticsearch 8.x,multi_match 全文检索)。BookLibrary 演示事件驱动的索引同步:图书库存变化经领域事件异步重索引,搜索结果的副本数实时准确。详见 全文搜索用法
  • 健康检查(k8s 探针):各依赖(MySQL/Redis/RabbitMQ/Elasticsearch)健康检查随包提供,AddSummerHealthChecks().Add...() 链式装配,MapSummerHealthChecks() 暴露 /health/live(存活)与 /health/ready(就绪)。详见 健康检查
  • 可观测性(OpenTelemetry 追踪 + 指标)AddSummerTracing() / AddSummerMetrics() 装配 OTel。追踪自动埋点 ASP.NET Core/HttpClient/DB,并在 RabbitMQ 发布/消费间传播 W3C traceparent,实现跨消息的端到端链路;指标采集 runtime/ASP.NET/HttpClient 及框架自身(RabbitMQ 收发/重试/死信/消费耗时)。详见 可观测性

项目结构

项目 职责
src/Summer.Core 核心抽象:IClockIGuidGenerator(顺序 Guid)等基础设施。
src/Summer.Domain DDD 构件:Entity/AggregateRootIRepository/IReadOnlyRepository、领域事件接口。
src/Summer.Uow 工作单元:IUnitOfWorkManager/IUnitOfWorkITransactionApi,事务资源与领域事件的统一提交/回滚编排。
src/Summer.EntityFrameworkCore EF Core 集成:SummerDbContext(并发戳 + 领域事件转发到 UoW)、DbContextProvider(按 UoW 复用 DbContext 与事务)、EfCoreRepository
src/Summer.Dapper Dapper 集成:IDbConnectionProvider 从当前 UoW 的 EF 事务里取出同一个连接/事务,供 Dapper 原生 SQL 复用。
src/Summer.Caching 分布式缓存抽象:IDistributedCache<TCacheItem>GetOrAddAsync、key 前缀策略。
src/Summer.Caching.StackExchangeRedis 缓存的 Redis 实现,注册单例 IConnectionMultiplexer
src/Summer.DistributedLocking 分布式锁抽象:IDistributedLock/IDistributedLockHandle
src/Summer.DistributedLocking.Redis 锁的 Redis 实现:SET NX PX + Lua 原子释放 + 看门狗续期。
src/Summer.EventBus 本地/分布式事件总线抽象与内存实现,ILocalEventHandler<T>/IDistributedEventHandler<T> 扫描注册。
src/Summer.EventBus.RabbitMQ 分布式事件总线的 RabbitMQ 实现:交换机/队列声明、发布、消费与反序列化分发。
src/Summer.Search 全文搜索抽象:ISearchService(按类型分索引,Index/Delete/Search)。
src/Summer.Search.Elasticsearch 搜索的 Elasticsearch 实现(官方 Elastic.Clients.Elasticsearch 8.x,multi_match 检索)。
src/Summer.Serialization 序列化抽象与 Json/Protobuf 实现,供缓存与 RabbitMQ 消息体使用。
src/Summer.AspNetCore ASP.NET Core 集成:SummerUnitOfWorkMiddleware 按请求开启/提交 UoW;健康检查聚合入口与探针端点映射。
src/Summer.Observability 可观测性:AddSummerTracing 一键装配 OpenTelemetry 追踪(ASP.NET/HttpClient/框架源/DB + OTLP/Console 导出)。
samples/Summer.Demo.Host 演示宿主(单目标 net8.0 可执行应用):Minimal API 串联以上全部能力,见下文。
samples/BookLibrary 更完整的分层 DDD 示例(图书馆:采购 / 借书 / 还书),HTTP API 用控制器 + Action,演示审计字段、审计日志、事件驱动的 Elasticsearch 索引同步;配套 test/BookLibrary.IntegrationTests 端到端测试。

架构总览

分层与包依赖(上层依赖下层;抽象与实现分包,用 AddSummerXxx 组装):

graph TD
    subgraph App["宿主 / 应用"]
        API["Summer.Demo.Host<br/>Minimal API"]
        ASP["Summer.AspNetCore<br/>UoW 请求中间件"]
    end
    subgraph Dom["领域层"]
        DOM["Summer.Domain<br/>Entity / AggregateRoot / IRepository<br/>IEvent / ISoftDelete"]
    end
    subgraph Infra["基础设施"]
        UOW["Summer.Uow"]
        EF["Summer.EntityFrameworkCore<br/>EfCoreRepository / SummerDbContext"]
        DAP["Summer.Dapper"]
        CACHE["Summer.Caching(+Redis)"]
        LOCK["Summer.DistributedLocking(+Redis)"]
        EB["Summer.EventBus<br/>本地事件"]
        MQ["Summer.EventBus.RabbitMQ<br/>分布式事件"]
    end
    subgraph Kernel["内核"]
        CORE["Summer.Core"]
        SER["Summer.Serialization<br/>Json / Protobuf"]
    end

    API --> ASP --> UOW
    API --> DOM
    EF --> DOM
    EF --> UOW
    DAP -. 复用同一连接/事务 .-> EF
    EB --> UOW
    MQ --> EB
    MQ --> SER
    CACHE --> SER
    DOM --> CORE
    UOW --> CORE

一次「下单」请求的 UoW 与事件流(分布式锁保护扣库存、EF+Dapper 同事务、本地事件提交前 / 分布式事件提交后):

sequenceDiagram
    autonumber
    participant C as Client
    participant MW as UoW 中间件
    participant EP as /orders 端点
    participant LK as 分布式锁(Redis)
    participant EF as EfCoreRepository
    participant DP as DapperRepository
    participant UOW as UnitOfWork
    participant MQ as RabbitMQ

    C->>MW: POST /orders
    MW->>UOW: Begin() 开启环境 UoW
    EP->>LK: TryAcquireAsync(product-stock)
    EP->>EF: Insert(order)(聚合根内 AddLocalEvent / AddDistributedEvent)
    EP->>UOW: SaveChangesAsync(EF flush 进事务)
    EP->>DP: Insert(orderLine)(同一连接/事务)
    EP->>UOW: CompleteAsync
    UOW->>UOW: SaveChanges
    UOW-->>EP: 发布本地事件(提交前,进程内)
    UOW->>UOW: Commit(MySQL 事务)
    UOW->>MQ: 发布分布式事件(提交后投递)
    MW-->>C: 200 OK(锁在提交后释放)

仓储用法

接口分层与方法归属

按「provider 无关」与「EF 专属」分层,避免把 EF 特有能力塞进通用契约(Dapper 也能实现通用契约):

接口 位置 提供的能力
IReadOnlyRepository<TEntity,TKey> Summer.Domain 只读:按主键/按条件 Get/FindGetListGetCountAny、分页 GetPagedListAsync(两种排序重载)。
IRepository<TEntity,TKey> Summer.Domain 在只读之上增加写:Insert/InsertManyUpdate/UpdateManyDelete/DeleteMany(含按条件加载式删除)。尊重软删除
IEfCoreRepository<TEntity,TKey> Summer.EntityFrameworkCore EF 专属:GetQueryableAsync(asNoTracking, includeDeleted)、带 IncludeFind/GetListHardDeleteAsync(物理删)、库内批量 UpdateDirectAsync/DeleteDirectAsyncExecuteUpdate/ExecuteDelete)。

「不加载实体、走 EF Execute* 的库内批量操作」(UpdateDirectAsync/DeleteDirectAsync)统一在 IEfCoreRepository,与逐条 CRUD 区分开。

条件查询与分页

// 按条件查询(都接受 Expression<Func<TEntity,bool>> predicate)
var one   = await repo.FindAsync(p => p.Name == "Widget");
var list  = await repo.GetListAsync(p => p.Stock > 0);
var count = await repo.GetCountAsync(p => p.Stock > 0);
var any   = await repo.AnyAsync(p => p.Stock == 0);

// 分页 —— 两种排序重载
var page1 = await repo.GetPagedListAsync(p => p.Stock > 0, skipCount: 0, maxResultCount: 20,
                                         orderBy: p => p.Stock, descending: true);   // 表达式排序
var page2 = await repo.GetPagedListAsync(predicate: null, 0, 20, sorting: "Name ASC, Stock DESC"); // 字符串排序

字符串排序用自实现的属性名白名单解析器(仅允许「属性名 + ASC/DESC」,未知属性抛异常),不引入 System.Linq.Dynamic.Core,无动态表达式注入面。

Include 与 AsNoTracking(IEfCoreRepository

// 完全灵活:拿到 IQueryable 自行 .Include().ThenInclude().Where()
var query = await efRepo.GetQueryableAsync(asNoTracking: true);
var blogs = await query.Include(b => b.Posts).ThenInclude(p => p.Comments).ToListAsync();

// 便捷重载:顶层 Include + 可选 no-tracking
var blog = await efRepo.FindAsync(id, asNoTracking: true,
    includes: new Expression<Func<Blog, object>>[] { b => b.Posts });

批量增删改

await repo.InsertManyAsync(items);
await repo.UpdateManyAsync(items);
await repo.DeleteManyAsync(items);                       // 按实体
await repo.DeleteManyAsync(p => p.Stock == 0);           // 按条件:加载后删除(尊重软删除)

// 库内批量(不加载实体,立即在当前 UoW 事务内执行)
await efRepo.UpdateDirectAsync(p => p.Stock < 10, s => s.SetProperty(p => p.Stock, 0));
await efRepo.DeleteDirectAsync(p => p.Stock == 0);       // 批量物理删除(ExecuteDelete)

软删除

实体实现 ISoftDelete { bool IsDeleted { get; set; } } 即自动生效,无需改任何删除调用SummerDbContext 在保存前把删除拦截为 IsDeleted = true,并为该实体自动加全局查询过滤器 !IsDeleted(与实体自设的其它过滤器 AND 组合,不覆盖)。

public class Article : AggregateRoot<Guid>, ISoftDelete
{
    public bool IsDeleted { get; set; }
    // ...
}

await repo.DeleteAsync(id);            // 软删除:置 IsDeleted=true
await repo.GetListAsync();             // 默认查询自动排除已删

// 需要看到/物理清除已删数据时:
var withDeleted = await (await efRepo.GetQueryableAsync(asNoTracking: true, includeDeleted: true)).ToListAsync();
await efRepo.HardDeleteAsync(article); // 强制物理删除(即使是软删实体)

删除语义一览(关键:不带前缀 = 尊重软删除;Hard.../...Direct = 绕过软删除的物理删除):

方法 归属 ISoftDelete 实体 对普通实体
DeleteAsync / DeleteManyAsync(entities) IRepository 软删除 物理删除
DeleteManyAsync(predicate) IRepository 加载后软删除 加载后物理删除
DeleteDirectAsync(predicate) IEfCoreRepository 批量物理删除(不加载、绕过软删) 批量物理删除
HardDeleteAsync(entity) IEfCoreRepository 强制物理删除 物理删除

对不实现 ISoftDelete 的实体,DeleteAsyncHardDeleteAsync 等价;HardDelete 仅在软删场景才有意义(真正清除已软删数据)。

仓储注册

services.AddSummerEfCore<AppDbContext>();
services.AddSummerEfCoreRepositories<AppDbContext>();          // 扫描 DbSet<T>,为每个聚合根注册默认仓储
services.AddSummerDapper<AppDbContext>();
services.AddSummerRepositories(Assembly.GetExecutingAssembly()); // 按约定注册自定义仓储(类名以 Repository 结尾)

EventBus 用法

所有事件 DTO 都实现标记接口 IEvent。本地事件在进程内分发;分布式事件经 RabbitMQ 跨进程投递(并可被本进程消费)。

本地事件

// 1) 事件 DTO
public record OrderPlacedEvent(Guid OrderId) : IEvent;

// 2) 处理器(按程序集扫描自动注册为 transient)
public class SendOrderNotificationHandler : ILocalEventHandler<OrderPlacedEvent>
{
    public Task HandleEventAsync(OrderPlacedEvent e, CancellationToken ct = default)
    {
        // ... 发通知 / 写缓存等进程内副作用
        return Task.CompletedTask;
    }
}

// 3a) 聚合根内产生(推荐):随 UoW 完成时统一发布
public class Order : AggregateRoot<Guid>
{
    public Order(Guid id)
    {
        Id = id;
        AddLocalEvent(new OrderPlacedEvent(id));
    }
}

// 3b) 或手动发布
await localEventBus.PublishAsync(new OrderPlacedEvent(orderId));

分布式事件(RabbitMQ,特性驱动)

事件 DTO 用 [DistributedEvent] 声明交换机/路由/序列化(发布方契约),Handler 用 [EventHandler] 声明队列名(消费方):

using ProtoBuf;
using Summer.EventBus;
using Summer.EventBus.RabbitMQ;
using Summer.Serialization;

// 1) 集成事件 DTO —— Protobuf 需要 [ProtoContract]/[ProtoMember]
[ProtoContract]
[DistributedEvent(
    ExchangeName    = "orders",
    ExchangeType    = RabbitMqExchangeType.Topic,   // direct / topic / fanout
    RoutingKey      = "order.created",
    SerializationType = SerializationType.Protobuf)] // 或 Json
public class OrderCreatedEto : IEvent
{
    [ProtoMember(1)] public Guid OrderId { get; set; }
    [ProtoMember(2)] public int Quantity { get; set; }
}

// 2) 分布式处理器 —— [EventHandler] 指定消费队列名
[EventHandler("order-service.order-created")]
public class OrderCreatedHandler : IDistributedEventHandler<OrderCreatedEto>
{
    public Task HandleEventAsync(OrderCreatedEto e, CancellationToken ct = default)
    {
        // ... 跨进程消费到的业务处理
        return Task.CompletedTask;
    }
}

// 3) 聚合根内产生(随 UoW 提交后投递),或手动 distributedEventBus.PublishAsync(eto)
AddDistributedEvent(new OrderCreatedEto { OrderId = id, Quantity = qty });

特性缺省时走命名约定:交换机 = 事件类型名、direct、routingKey = 事件类型名、Json 序列化;队列 = {ClientName}:{事件类型名}。同一队列名被两个不同事件占用会在注册时抛出清晰的配置异常。

与 UoW 的关系

聚合根通过 AddLocalEvent / AddDistributedEvent 收集的事件缓冲在当前 IUnitOfWork,由 UoW 完成时按顺序发布:本地事件在提交前(供强一致的进程内副作用),分布式事件在提交后(避免回滚后仍对外投递)。

注册

// 本地事件总线 + 扫描注册本程序集里的 ILocalEventHandler/IDistributedEventHandler
services.AddSummerEventBus(Assembly.GetExecutingAssembly());

// 启用 RabbitMQ:扫描分布式 Handler,从事件/Handler 特性自动建订阅,
// 并用 RabbitMQ 实现替换进程内的 IDistributedEventBus。不需要逐个事件手动注册。
services.AddSummerRabbitMq(o =>
{
    o.Connection = "amqp://guest:guest@localhost:5672";
    o.ClientName = "order-service";
}, Assembly.GetExecutingAssembly());

断线重连与恢复

消费端断线重连采用三层策略,默认开箱可用,全部经 SummerRabbitMqOptions 可调:

  1. 运行期自愈(首选):连接工厂开启 RabbitMQ.Client 的自动连接恢复 + 拓扑恢复——断线后客户端自动重连,并重声明交换机/队列/绑定、重注册消费者,消费者透明续投,无需自写重连循环。
  2. 启动韧性:消费者 StartAsync 对"初次连接+声明+订阅"做有界退避重试,应对宿主启动时 broker 尚未就绪。
  3. 连接单例 + 可观测:连接池持有同一个自恢复连接并持续复用(不因恢复窗口内 IsOpen 瞬时为 false 而重建,避免重复连接);挂接关闭/恢复事件打日志(connection shutdown / connection recovered),便于观测。
services.AddSummerRabbitMq(o =>
{
    o.Connection = "amqp://guest:guest@localhost:5672";
    o.ClientName  = "order-service";

    // 运行期自愈(默认开启)
    o.AutomaticRecoveryEnabled = true;                       // 断线自动重连
    o.TopologyRecoveryEnabled  = true;                       // 重连后重建拓扑并重订阅
    o.NetworkRecoveryInterval  = TimeSpan.FromSeconds(5);    // 重连重试间隔
    o.RequestedHeartbeat       = TimeSpan.FromSeconds(30);   // 心跳,及时探测断线

    // 启动重试(broker 未就绪时)
    o.StartupRetryCount    = 10;
    o.StartupRetryInterval = TimeSpan.FromSeconds(3);
}, Assembly.GetExecutingAssembly());

说明:发布端在恢复窗口内 publish 可能瞬时失败(当前提交后发布不重试),真正的兜底是 Outbox(业务与消息同事务落库、后台中继投递)——见「遗留 / 生产强化建议」。生产环境可重启 broker,观察日志中的 connection recovered 确认自愈生效。

消息可靠性(预取 / 死信 / 重试 / 幂等)

消费端默认不再"处理失败即丢弃",而是重试 → 死信,配合幂等去重,达到 at-least-once:

  • 预取流控(QoS)PrefetchCount(默认 10)限制每个消费者未 ack 的在途消息数——流控 + 多实例公平分发,避免单消费者抢光队列。
  • 消息持久化:发布的消息带 Persistent(delivery mode 2)+ 队列/交换机 durable,broker 重启不丢。
  • 失败重试 + 死信(不丢弃):handler 抛异常时先进程内有界退避重试 MaxRetryCount 次(第 n 次等待 RetryInterval × n);仍失败则 nack 不重回主队列,经 x-dead-letter-exchange 落到死信队列 {队列名}.dead(可排查/重放),而非直接丢弃。
  • 幂等去重(Inbox):每条消息发布时自动带唯一 MessageId;消费端按 MessageId 去重(IProcessedMessageStore),跳过重复投递,避免重复副作用。at-least-once + 重试必然产生重复,handler 仍应保证幂等
services.AddSummerRabbitMq(o =>
{
    o.Connection = "amqp://guest:guest@localhost:5672";
    o.ClientName = "order-service";

    o.PrefetchCount     = 10;                          // QoS 预取上限
    o.DeadLetterEnabled = true;                         // 失败消息进死信队列而非丢弃
    o.MaxRetryCount     = 3;                            // 进程内重试次数
    o.RetryInterval     = TimeSpan.FromSeconds(1);      // 退避基数(第 n 次等待 × n)
}, Assembly.GetExecutingAssembly());

去重存储:默认 InMemoryProcessedMessageStore 仅在单实例内去重。多实例部署应实现 IProcessedMessageStore(如基于 Redis 的 SET NX + TTL)并通过 DI 覆盖默认注册。

死信队列声明注意:开启 DeadLetterEnabled 会给主队列声明 x-dead-letter-exchange 参数;若已存在同名但无此参数的旧队列,需先删除旧队列再启动(RabbitMQ 队列参数不可变更)。

缓存与分布式锁用法

缓存与分布式锁共用同一个单例 IConnectionMultiplexer(由 AddSummerStackExchangeRedis 注册)。

分布式缓存

// 缓存项类型(强类型;key 自动加 前缀 + 类型名 隔离)
public class ProductCacheItem
{
    public string Name { get; set; } = "";
    public int Stock { get; set; }
}

// 注册
services.AddSummerCaching(o => o.KeyPrefix = "demo:");
services.AddSummerStackExchangeRedis(o => o.Configuration = "localhost:6379");

// 注入 IDistributedCache<ProductCacheItem> 使用
var item = await cache.GetOrAddAsync(
    id.ToString(),
    async () =>                       // 缺失时用工厂加载并回填(工厂只会被调用一次)
    {
        var p = await repo.GetAsync(id);
        return new ProductCacheItem { Name = p.Name, Stock = p.Stock };
    },
    new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(10) });

await cache.SetAsync(id.ToString(), item);
await cache.RemoveAsync(id.ToString());

分布式锁

// 注册(复用上面缓存注册的同一 IConnectionMultiplexer)
services.AddSummerRedisDistributedLock();

// 注入 IDistributedLock 使用
await using var handle = await distributedLock.TryAcquireAsync(
    key:    $"product-stock:{productId}",
    expiry: TimeSpan.FromSeconds(30),    // 锁最长持有时间(持锁期间看门狗自动续期,防长任务被误释放)
    wait:   TimeSpan.FromSeconds(5),     // 最多等待 5s
    retry:  TimeSpan.FromMilliseconds(200)); // 抢锁重试间隔

if (handle is null)
{
    // 未在等待时间内抢到锁
    return Results.StatusCode(StatusCodes.Status409Conflict);
}

// 临界区:扣库存等。离开 using 作用域时用 Lua 脚本校验持有者 token 后原子释放。

实现:SET key token NX PX <expiry> 抢锁;释放用 Lua 脚本 if get==token then del(避免误删他人重获的锁);看门狗按 expiry/3(不超过 expiry/2)间隔续期。单节点精简版 Redlock。

全文搜索(Elasticsearch)用法

抽象 ISearchServiceSummer.Search)按文档类型分索引(索引名 = {IndexPrefix}{类型名} 小写):

public interface ISearchService
{
    Task IndexAsync<TDocument>(string id, TDocument document, CancellationToken ct = default) where TDocument : class;
    Task DeleteAsync<TDocument>(string id, CancellationToken ct = default) where TDocument : class;
    Task<IReadOnlyList<TDocument>> SearchAsync<TDocument>(string query, IEnumerable<string> fields, int size = 20, CancellationToken ct = default) where TDocument : class;
}

注册(Summer.Search.Elasticsearch,单例 ElasticsearchClient,写入 Refresh.WaitFor 即写即搜):

services.AddSummerElasticsearch(o => o.Uri = "http://localhost:9200");
// 注入 ISearchService 使用:
await search.IndexAsync("book-id", new BookSearchDocument { Title = "DDD", Author = "Evans", ... });
var hits = await search.SearchAsync<BookSearchDocument>("ddd", fields: new[] { "title", "author" });

事件驱动的索引同步(BookLibrary 演示)

搜索索引是读模型,必须随写模型变化而更新,否则搜到的是陈旧数据。BookLibrary 用领域事件异步驱动索引,与业务主流程解耦:

  • Book 聚合在每次库存变化(采购 / 借出 / 归还)时发 BookStockChangedEto(仅带 BookId);下架时发 BookRetiredEto
  • 索引 Handler 订阅这些集成事件(经 RabbitMQ,事务提交后投递):
    • IndexBookOnStockChangedHandler:收到库存变化事件后重新读取图书当前提交状态(Include 副本),把最新 TotalCopies/AvailableCopies 索引进 ES —— 因为读的是提交后的真实值,搜索结果的副本数始终准确、动态。
    • RemoveBookFromIndexHandler:收到下架事件后把该书从索引移除(下架后搜不到)。
  • 搜索 API GET /api/books/search?q= 返回的 TotalCopies/AvailableCopies 即索引中的最新值。

一致性:索引更新经 RabbitMQ 最终一致(异步)——高并发下搜索到的副本数可能有极短暂延迟,随后收敛到真实值,这是全文搜索的标准取舍。若需强一致,可改为应用服务内同步索引(牺牲解耦)。端到端测试 LibraryFlowTests 验证了「采购→借2→还1」后索引里的可借数动态同步为真实值 1。

健康检查(k8s 探针)

各依赖的健康检查随其所在包提供(复用已注册的连接单例),Summer.AspNetCore 提供聚合入口与端点映射。约定用 ready 标签区分“就绪(依赖可达)”检查;存活探针不检查任何依赖。

// 注册:按已启用的依赖链式追加检查(各检查默认打 ready 标签)
builder.Services.AddSummerHealthChecks()
    .AddSummerDbContextCheck<AppDbContext>()   // Summer.EntityFrameworkCore:Database.CanConnectAsync
    .AddSummerRedisCheck()                      // Summer.Caching.StackExchangeRedis:PING
    .AddSummerRabbitMqCheck()                   // Summer.EventBus.RabbitMQ:连接是否打开
    .AddSummerElasticsearchCheck();             // Summer.Search.Elasticsearch:Ping

// 映射两个探针端点
app.MapSummerHealthChecks();   // 默认 /health/live 与 /health/ready
  • /health/live(liveness):不检查依赖,进程能响应即 200——失败则 k8s 重启 Pod。
  • /health/ready(readiness):所有 ready 标签的依赖检查全部健康才 200——失败则从负载均衡摘除,不打流量进来。
# k8s 探针示例
livenessProbe:
  httpGet: { path: /health/live, port: 8080 }
readinessProbe:
  httpGet: { path: /health/ready, port: 8080 }

BookLibrary 已接入四项检查;端到端测试验证四个依赖(MySQL/Redis/RabbitMQ/Elasticsearch)全部就绪时 /health/ready 返回 Healthy

可观测性(OpenTelemetry 追踪 + 指标)

Summer.Observability 提供 AddSummerTracing()AddSummerMetrics() 两个一键装配入口,二者可同时使用;导出器均支持控制台(开发)与 OTLP(生产,导 Jaeger/Tempo/Prometheus 等)。

追踪(Tracing)

AddSummerTracing() 自动埋点 ASP.NET Core 与 HttpClient,订阅框架自身的 Summer.* 源与 MySqlConnector(DB 跨度)。

builder.Services.AddSummerTracing(o =>
{
    o.ServiceName        = "book-library";
    o.UseConsoleExporter = true;                    // 开发期直接在终端看 span
    o.OtlpEndpoint       = "http://localhost:4317"; // 生产导出到 collector(为空则不加)
});

跨消息的端到端链路:RabbitMQ 发布时在消息头注入 W3C traceparent,消费端取出作为父上下文——一条 trace 可从 HTTP 请求 → 发布事件 → broker → 另一进程消费 完整串联。该埋点内建于 Summer.EventBus.RabbitMQ(ActivitySource Summer.EventBus.RabbitMQ,仅依赖运行时 Activity,不强加 OTel 依赖);是否导出取决于是否配置了 AddSummerTracing

端到端测试验证:消费端 handler 运行在与发布端相同的 TraceId 下(跨进程链路串联生效)。

指标(Metrics)

AddSummerMetrics() 采集 .NET runtime(GC/线程池/异常)、ASP.NET Core、HttpClient 指标,并订阅框架自身的 Summer.* Meter。框架内建的 RabbitMQ 指标(Meter Summer.EventBus.RabbitMQ,仅依赖运行时 Meter,不强加 OTel 依赖):

指标 类型 含义
summer.rabbitmq.published Counter 已发布的分布式事件数
summer.rabbitmq.consumed Counter 成功消费的分布式事件数
summer.rabbitmq.retries Counter 消费处理的重试次数
summer.rabbitmq.dead_lettered Counter 重试用尽转入死信的消息数
summer.rabbitmq.consume.duration Histogram 单条消息的消费处理耗时(ms)
builder.Services.AddSummerTracing(o => { o.ServiceName = "book-library"; o.UseConsoleExporter = true; });
builder.Services.AddSummerMetrics(o => { o.ServiceName = "book-library"; o.OtlpEndpoint = "http://localhost:4317"; });

端到端测试验证:一次发布→消费后,summer.rabbitmq.publishedsummer.rabbitmq.consumed 计数均 > 0。

构建与测试

dotnet build
dotnet test
  • 库项目多目标 net8.0;net9.0;net10.0dotnet test 会对每个 TFM 各跑一遍。
  • Redis / RabbitMQ / MySQL / Elasticsearch 相关的集成测试使用 Testcontainers 启动真实依赖;本机没有 Docker(或 Docker 未运行)时这些测试会自动 跳过SkippableFact),不会导致失败。有 Docker 时会真实起容器验证(含 BookLibrary 端到端)。

运行 Demo

samples/Summer.Demo.Host 演示了框架的完整用法:分布式锁保护扣库存、EF+Dapper 同一 UoW 事务写聚合与流水、缓存 GetOrAdd、本地事件与经 RabbitMQ 的分布式事件(Protobuf 序列化)。

  1. 启动依赖(MySQL + Redis + RabbitMQ):
cd samples
docker compose up -d
  1. 运行宿主:
dotnet run --project samples/Summer.Demo.Host
  1. 调用示例:
# 建商品
curl -X POST http://localhost:5080/products -H "Content-Type: application/json" \
  -d '{"name":"Widget","stock":100}'

# 下单(分布式锁扣库存 + EF/Dapper 同事务写 + 本地/分布式事件)
curl -X POST http://localhost:5080/orders -H "Content-Type: application/json" \
  -d '{"productId":"<上面返回的 id>","quantity":3}'

# 查商品(走 Redis 缓存 GetOrAdd)
curl http://localhost:5080/products/<id>

Demo 用 EnsureCreated 建表,仅为演示方便;生产环境请用 EF Core 迁移。

设计要点

  • 不引入模块系统:用原生 DI 扩展方法(AddSummerCore/AddSummerUow/AddSummerEfCore/... )组装,显式、可读、无需理解额外的模块生命周期概念。
  • EF + Dapper 同一事务的原理DbContextProvider 在当前 UoW 内首次被请求时创建 DbContext 并开启数据库事务,把它包成 EfCoreTransactionApi 注册进 UoW(key = DbContext 类型名);IDbConnectionProvider 从同一个 DbContextProvider 取出正在使用的 DbConnection/DbTransaction,因此 Dapper 的原生 SQL 与 EF 的变更落在同一个数据库事务里,天然共提交/共回滚。
  • UoW 完成顺序SaveChangesAsync(把 EF 变更落到当前事务)→ 发布本地事件(进程内,供提交前的强一致性副作用如写缓存/发通知)→ 底层事务 CommitAsync(真正落库)→ 发布分布式事件(提交后才对外投递,避免事务回滚后仍通知外部系统)。

更多细节见:

  • 设计文档:docs/superpowers/specs/2026-07-09-summer-frame-design.md
  • 分阶段实施计划:docs/superpowers/plans/(Phase 1 基础设施 → Phase 2 仓储 → Phase 3 Redis → Phase 4 EventBus → Phase 5 RabbitMQ → Phase 6 ASP.NET Core 中间件 + Demo + 集成测试)
  • 增强计划:docs/superpowers/plans/2026-07-09-enhance-a-repositories.md(丰富仓储方法 + 自动注册)、enhance-b-eventbus.mdIEvent + 特性驱动 RabbitMQ)、2026-07-10-enhance-c-efcore-query.md(Include/AsNoTracking + 软删除 + 批量更新)

遗留 / 生产强化建议

  • Demo 用 EnsureCreated 建表;生产应用 EF 迁移。
  • 发布端 Outbox + publisher confirms:消除"commit 成功但 publish 丢"的双写窗口(框架级已预留 IUnitOfWorkEventPublisher 扩展点)。消费端可靠性(重试/死信/幂等/重连)已具备。
  • 可观测性补全:OTel Logs 与追踪/指标关联(追踪、指标已具备;缓存命中/锁等待等业务指标可继续按需扩展)。
  • 默认幂等去重为单实例内存版;多实例需实现分布式 IProcessedMessageStore(如 Redis)。
  • Redis 连接在注册时同步建立,且不随 DI 容器释放(已知权衡)。
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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 (4)

Showing the top 4 NuGet packages that depend on SummerFrame.Uow:

Package Downloads
SummerFrame.EventBus

Summer.EventBus —— Summer.Frame 精简 .NET 框架组件(DDD / 仓储 / UoW / 缓存 / 分布式锁 / EventBus / 全文搜索)。

SummerFrame.EntityFrameworkCore

Summer.EntityFrameworkCore —— Summer.Frame 精简 .NET 框架组件(DDD / 仓储 / UoW / 缓存 / 分布式锁 / EventBus / 全文搜索)。

SummerFrame.Dapper

Summer.Dapper —— Summer.Frame 精简 .NET 框架组件(DDD / 仓储 / UoW / 缓存 / 分布式锁 / EventBus / 全文搜索)。

SummerFrame.AspNetCore

Summer.AspNetCore —— Summer.Frame 精简 .NET 框架组件(DDD / 仓储 / UoW / 缓存 / 分布式锁 / EventBus / 全文搜索)。

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 210 7/20/2026
0.1.0 204 7/13/2026