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

作者:微信公众号:【架构师老卢】
6-7 9:15
6

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

GetByIdWithCreatorAndAttendeesAsync(...)
GetByNameWithEverythingAsync(...)
GetSplitQueryByIdWithStuffAsync(...)

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

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

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

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

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

public async Task<Gathering?> GetByIdAsync(Guid id)
{
    return await _db.Gatherings
        .Include(g => g.Creator)
        .Include(g => g.Attendees)
        .FirstOrDefaultAsync(g => g.Id == id);
}

public async Task<Gathering?> GetByNameAsync(string name)
{
    return await _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 基础规范类

// /Specifications/Specification.cs
public abstract class Specification<TEntity>
{
    public Expression<Func<TEntity, bool>>? Criteria { get; protected set; }
    public List<Expression<Func<TEntity, object>>> Includes { get; } = [];
    public Expression<Func<TEntity, object>>? OrderBy { get; protected set; }
    public Expression<Func<TEntity, object>>? OrderByDescending { get; protected set; }
    public bool IsSplitQuery { get; protected set; }
    
    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 具体规范实现

// GatheringByIdWithCreatorSpec.cs
public sealed class GatheringByIdWithCreatorSpec : Specification<Gathering>
{
    public GatheringByIdWithCreatorSpec(Guid id)
    {
        Criteria = g => g.Id == id;
        AddInclude(g => g.Creator);
    }
}

// GatheringByNameSpec.cs
public sealed class GatheringByNameSpec : 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
public sealed class GatheringSplitSpec : 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 规范评估器

public static class SpecificationEvaluator
{
    public static IQueryable<TEntity> GetQuery<TEntity>(
        IQueryable<TEntity> input, Specification<TEntity> spec)
        where TEntity : class
    {
        if (spec.Criteria is not null)
            input = input.Where(spec.Criteria);
        
        foreach (var inc in spec.Includes)
            input = input.Include(inc);
        
        if (spec.OrderBy is not null)
            input = input.OrderBy(spec.OrderBy);
        else if (spec.OrderByDescending is not null)
            input = input.OrderByDescending(spec.OrderByDescending);
        
        if (spec.IsSplitQuery)
            input = input.AsSplitQuery();
        
        return input;
    }
}

3.4 重构仓储

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);
}

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

实战演示

// 控制器
[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;而不是查询细节

生产环境增强

分页支持

public int? Skip { get; private set; }
public int? Take { get; private set; }
protected void ApplyPaging(int skip, int take) =>
    (Skip, Take) = (skip, take);

添加到评估器:

if (spec.Skip.HasValue) query = query.Skip(spec.Skip.Value);
if (spec.Take.HasValue) query = query.Take(spec.Take.Value);

只读与命令分离

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

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

🚀 关键要点

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

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

相关留言评论
昵称:
邮箱:
阅读排行