SummerFrame.AspNetCore
0.2.0
dotnet add package SummerFrame.AspNetCore --version 0.2.0
NuGet\Install-Package SummerFrame.AspNetCore -Version 0.2.0
<PackageReference Include="SummerFrame.AspNetCore" Version="0.2.0" />
<PackageVersion Include="SummerFrame.AspNetCore" Version="0.2.0" />
<PackageReference Include="SummerFrame.AspNetCore" />
paket add SummerFrame.AspNetCore --version 0.2.0
#r "nuget: SummerFrame.AspNetCore, 0.2.0"
#:package SummerFrame.AspNetCore@0.2.0
#addin nuget:?package=SummerFrame.AspNetCore&version=0.2.0
#tool nuget:?package=SummerFrame.AspNetCore&version=0.2.0
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),乐观并发戳ConcurrencyStamp由SummerDbContext自动维护。 - 仓储(EF Core + Dapper 双实现,同一 UoW 共享事务):
IReadOnlyRepository/IRepository提供 provider 无关的丰富方法(按主键/按条件Get/Find/GetList/GetCount/Any、分页 + 排序、批量增改删);EF 专属能力(Include/AsNoTracking、软删除、库内批量Update/Delete)在IEfCoreRepository。IDbConnectionProvider让 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.TryAcquireAsync,SET NX PX抢锁 + Lua 脚本保证释放的原子性(校验持有者 token)+ 看门狗自动续期,避免长任务持锁期间因过期被误释放。 - EventBus(本地 + 分布式):所有事件 DTO 实现标记接口
IEvent;ILocalEventHandler<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.Elasticsearch8.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 发布/消费间传播 W3Ctraceparent,实现跨消息的端到端链路;指标采集 runtime/ASP.NET/HttpClient 及框架自身(RabbitMQ 收发/重试/死信/消费耗时)。详见 可观测性。
项目结构
| 项目 | 职责 |
|---|---|
src/Summer.Core |
核心抽象:IClock、IGuidGenerator(顺序 Guid)等基础设施。 |
src/Summer.Domain |
DDD 构件:Entity/AggregateRoot、IRepository/IReadOnlyRepository、领域事件接口。 |
src/Summer.Uow |
工作单元:IUnitOfWorkManager/IUnitOfWork、ITransactionApi,事务资源与领域事件的统一提交/回滚编排。 |
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/Find、GetList、GetCount、Any、分页 GetPagedListAsync(两种排序重载)。 |
IRepository<TEntity,TKey> |
Summer.Domain |
在只读之上增加写:Insert/InsertMany、Update/UpdateMany、Delete/DeleteMany(含按条件加载式删除)。尊重软删除。 |
IEfCoreRepository<TEntity,TKey> |
Summer.EntityFrameworkCore |
EF 专属:GetQueryableAsync(asNoTracking, includeDeleted)、带 Include 的 Find/GetList、HardDeleteAsync(物理删)、库内批量 UpdateDirectAsync/DeleteDirectAsync(ExecuteUpdate/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的实体,DeleteAsync与HardDeleteAsync等价;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 可调:
- 运行期自愈(首选):连接工厂开启 RabbitMQ.Client 的自动连接恢复 + 拓扑恢复——断线后客户端自动重连,并重声明交换机/队列/绑定、重注册消费者,消费者透明续投,无需自写重连循环。
- 启动韧性:消费者
StartAsync对"初次连接+声明+订阅"做有界退避重试,应对宿主启动时 broker 尚未就绪。 - 连接单例 + 可观测:连接池持有同一个自恢复连接并持续复用(不因恢复窗口内
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)用法
抽象 ISearchService(Summer.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.published与summer.rabbitmq.consumed计数均 > 0。
构建与测试
dotnet build
dotnet test
- 库项目多目标
net8.0;net9.0;net10.0,dotnet test会对每个 TFM 各跑一遍。 - Redis / RabbitMQ / MySQL / Elasticsearch 相关的集成测试使用 Testcontainers 启动真实依赖;本机没有 Docker(或 Docker 未运行)时这些测试会自动 跳过(
SkippableFact),不会导致失败。有 Docker 时会真实起容器验证(含 BookLibrary 端到端)。
运行 Demo
samples/Summer.Demo.Host 演示了框架的完整用法:分布式锁保护扣库存、EF+Dapper 同一 UoW 事务写聚合与流水、缓存 GetOrAdd、本地事件与经 RabbitMQ 的分布式事件(Protobuf 序列化)。
- 启动依赖(MySQL + Redis + RabbitMQ):
cd samples
docker compose up -d
- 运行宿主:
dotnet run --project samples/Summer.Demo.Host
- 调用示例:
# 建商品
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.md(IEvent+ 特性驱动 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 | Versions 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. |
-
net10.0
- SummerFrame.Uow (>= 0.2.0)
-
net8.0
- SummerFrame.Uow (>= 0.2.0)
-
net9.0
- SummerFrame.Uow (>= 0.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.