这是一个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();
}
问题所在:
规范 = 描述查询的对象:
定义一次 → 传递给评估器 → 获得IQueryable
// /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;
}
// 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进行分页查询
}
}
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;
}
}
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();按规范切换。
在评估器内部缓存已编译查询以提高性能。 通过扩展方法动态组合——使用运算符(&&, ||)组合规范。
下次当你发现自己要写"GetByXWithYAndZ"时,改为编写一个规范——你未来的自己(和团队成员)会感谢你。