SummerFrame.Dapper 0.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package SummerFrame.Dapper --version 0.1.0
                    
NuGet\Install-Package SummerFrame.Dapper -Version 0.1.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.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SummerFrame.Dapper" Version="0.1.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.1.0
                    
#r "nuget: SummerFrame.Dapper, 0.1.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.1.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.1.0
                    
Install as a Cake Addin
#tool nuget:?package=SummerFrame.Dapper&version=0.1.0
                    
Install as a Cake Tool

Summer.Frame

一个精简的 .NET 框架:DDD 领域模型、EF Core + Dapper 双仓储(同一 UoW 共享事务)、工作单元(UoW)、分布式缓存(Redis)、分布式锁(Redis 自实现)、本地/分布式事件总线(RabbitMQ)、全文搜索(Elasticsearch)。库多目标 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 自动建订阅,无需逐个事件手动注册。消费端只订阅本进程注册了处理器的事件类型。
  • 全文搜索(Elasticsearch)ISearchService(provider 无关:按类型分索引,Index/Delete/Search),实现 Summer.Search.Elasticsearch(官方 Elastic.Clients.Elasticsearch 8.x,multi_match 全文检索)。BookLibrary 演示事件驱动的索引同步:图书库存变化经领域事件异步重索引,搜索结果的副本数实时准确。详见 全文搜索用法

项目结构

项目 职责
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。
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 确认自愈生效。

缓存与分布式锁用法

缓存与分布式锁共用同一个单例 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。

构建与测试

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 可靠投递(发布端 at-least-once;框架级已预留 IUnitOfWorkEventPublisher 扩展点)。
  • 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