首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >规范模式:终结仓储臃肿的终极武器(C#实战)

规范模式:终结仓储臃肿的终极武器(C#实战)

作者头像
郑子铭
发布2025-07-26 10:44:44
发布2025-07-26 10:44:44
10300
代码可运行
举报
运行总次数:0
代码可运行

这是一个API发布后的清晨。原本简单的新端点——"通过创建者名称获取聚会并包含参与者、邀请函和创建者"——却在GatheringRepository中膨胀成了又一个新方法。突然间,你发现自己面对着这样的方法群:

代码语言:javascript
代码运行次数:0
运行
复制
GetByIdWithCreatorAndAttendeesAsync(...)
GetByNameWithEverythingAsync(...)
GetSplitQueryByIdWithStuffAsync(...)

一个实体,五个方法,无尽的Include()链。是不是有种维护噩梦似曾相识的感觉?

如果你认为这些额外的仓储重载方法无害,那只是因为你还没有在整个解决方案中搜索过重复的Include(g => g.Creator)。

想象一下,如果你能一次性封装所有查询逻辑——过滤器、包含项、排序、分页查询标志——使其可重用、可组合、可测试。你的仓储类将缩减为一行:"应用规范"。

突然间,规范模式不再只是设计模式手册中的脚注,而成为了对抗仓储臃肿的秘密武器。

痛点:重复的查询管道代码

代码语言:javascript
代码运行次数:0
运行
复制
public async Task<Gathering?> GetByIdAsync(Guid id)
{
    returnawait _db.Gatherings
        .Include(g => g.Creator)
        .Include(g => g.Attendees)
        .FirstOrDefaultAsync(g => g.Id == id);
}

publicasync Task<Gathering?> GetByNameAsync(string name)
{
    returnawait _db.Gatherings
        .Include(g => g.Creator)
        .Include(g => g.Attendees)
        .Where(g => g.Name == name)
        .OrderBy(g => g.Name)
        .FirstOrDefaultAsync();
}

问题所在:

  • 重复:相同的Include/OrderBy逻辑无处不在
  • 僵化:添加分页查询、分页或额外过滤器就需要新方法
  • 测试噩梦:你通过模拟EF来单元测试查询组合?糟糕透顶

规范模式登场

规范 = 描述查询的对象:

  • • 条件(Expression<Func<TEntity, bool>>)
  • • 包含列表
  • • 排序
  • • 额外标志(分页查询、分页...)

定义一次 → 传递给评估器 → 获得IQueryable

逐步构建规范模式

3.1 基础规范类

代码语言:javascript
代码运行次数:0
运行
复制
// /Specifications/Specification.cs
publicabstractclassSpecification<TEntity>
{
    public Expression<Func<TEntity, bool>>? Criteria { get; protectedset; }
    public List<Expression<Func<TEntity, object>>> Includes { get; } = [];
    public Expression<Func<TEntity, object>>? OrderBy { get; protectedset; }
    public Expression<Func<TEntity, object>>? OrderByDescending { get; protectedset; }
    publicbool IsSplitQuery { get; protectedset; }
    
    protected void AddInclude(Expression<Func<TEntity, object>> include) =>
        Includes.Add(include);
    
    protected void AddOrderBy(Expression<Func<TEntity, object>> order) =>
        OrderBy = order;
    
    protected void AddOrderByDescending(Expression<Func<TEntity, object>> order) =>
        OrderByDescending = order;
    
    protected void EnableSplitQuery() => IsSplitQuery = true;
}

3.2 具体规范实现

代码语言:javascript
代码运行次数:0
运行
复制
// GatheringByIdWithCreatorSpec.cs
publicsealedclassGatheringByIdWithCreatorSpec : Specification<Gathering>
{
    public GatheringByIdWithCreatorSpec(Guid id)
    {
        Criteria = g => g.Id == id;
        AddInclude(g => g.Creator);
    }
}

// GatheringByNameSpec.cs
publicsealedclassGatheringByNameSpec : Specification<Gathering>
{
    public GatheringByNameSpec(string name)
    {
        Criteria = g => g.Name == name;
        AddInclude(g => g.Creator);
        AddInclude(g => g.Attendees);
        AddOrderBy(g => g.Name);
    }
}

// GatheringSplitSpec.cs
publicsealedclassGatheringSplitSpec : Specification<Gathering>
{
    public GatheringSplitSpec(Guid id)
    {
        Criteria = g => g.Id == id;
        AddInclude(g => g.Creator);
        AddInclude(g => g.Attendees);
        AddInclude(g => g.Invitations);
        EnableSplitQuery(); // ⚡ 告诉EF进行分页查询
    }
}

3.3 规范评估器

代码语言:javascript
代码运行次数:0
运行
复制
public staticclassSpecificationEvaluator
{
    public static IQueryable<TEntity> GetQuery<TEntity>(
        IQueryable<TEntity> input, Specification<TEntity> spec)
        where TEntity : class
    {
        if (spec.Criteria isnotnull)
            input = input.Where(spec.Criteria);
        
        foreach (var inc in spec.Includes)
            input = input.Include(inc);
        
        if (spec.OrderBy isnotnull)
            input = input.OrderBy(spec.OrderBy);
        elseif (spec.OrderByDescending isnotnull)
            input = input.OrderByDescending(spec.OrderByDescending);
        
        if (spec.IsSplitQuery)
            input = input.AsSplitQuery();
        
        return input;
    }
}

3.4 重构仓储

代码语言:javascript
代码运行次数:0
运行
复制
public class GatheringRepository : IGatheringRepository
{
    private readonly AppDbContext _db;
    
    public GatheringRepository(AppDbContext db) => _db = db;
    
    private IQueryable<Gathering> Apply(Specification<Gathering> spec) =>
        SpecificationEvaluator.GetQuery(_db.Gatherings.AsQueryable(), spec);
    
    public Task<Gathering?> GetAsync(Specification<Gathering> spec, CancellationToken ct) =>
        Apply(spec).FirstOrDefaultAsync(ct);
}

现在仓储只有一个公共读取方法。添加规范,而不是方法。

实战演示

代码语言:javascript
代码运行次数:0
运行
复制
// 控制器
[HttpGet("{id}")]
public async Task<IActionResult> Get(Guid id)
{
    var spec = new GatheringSplitSpec(id);
    var gathering = await _gatheringRepo.GetAsync(spec, HttpContext.RequestAborted);
    return gathering is null ? NotFound() : Ok(gathering);
}

按F5 → SQL Profiler显示三个分页查询(创建者、参与者、邀请函)自动执行。

为什么这很酷

优势

说明

单一职责

仓储=协调者;规范=查询定义

可重用性

将小规范组合成更大的规范

可测试性

隔离单元测试规范:提供假IQueryable并断言表达式树

灵活性

分页?添加到基类。缓存查询?装饰评估器

关注点分离

控制器知道意图:GatheringByNameSpec;而不是查询细节

生产环境增强

分页支持

代码语言:javascript
代码运行次数:0
运行
复制
public int? Skip { get; private set; }
public int? Take { get; private set; }
protected void ApplyPaging(int skip, int take) =>
    (Skip, Take) = (skip, take);

添加到评估器:

代码语言:javascript
代码运行次数:0
运行
复制
if (spec.Skip.HasValue) query = query.Skip(spec.Skip.Value);
if (spec.Take.HasValue) query = query.Take(spec.Take.Value);

只读与命令分离

在读取规范中默认使用AsNoTracking();按规范切换。

在评估器内部缓存已编译查询以提高性能。 通过扩展方法动态组合——使用运算符(&&, ||)组合规范。

🚀 关键要点

  • • 规范模式集中查询逻辑,消除仓储重复
  • • 评估器将规范转换为IQueryable;仓储只需执行
  • • 具体规范=人类可读的意图(GatheringByNameSpec)
  • • 扩展分页、缓存或软删除过滤器——无需修改仓储代码

下次当你发现自己要写"GetByXWithYAndZ"时,改为编写一个规范——你未来的自己(和团队成员)会感谢你。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-07-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 痛点:重复的查询管道代码
  • 规范模式登场
  • 逐步构建规范模式
    • 3.1 基础规范类
    • 3.2 具体规范实现
    • 3.3 规范评估器
    • 3.4 重构仓储
  • 实战演示
  • 为什么这很酷
  • 生产环境增强
    • 分页支持
    • 只读与命令分离
  • 🚀 关键要点
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档