SummerFrame.Dapper 0.2.0

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 124 7/20/2026
0.1.0 109 7/13/2026