如何基于CQRS和MediatR实现Command Handler剥离查询逻辑?
小前置优化建议
你当前的GetMatchesCommand本质是查询请求,按照CQRS通用规范更建议命名为GetMatchesQuery,避免和写操作的Command混淆。另外你原代码中的query.Where(filterExpression)没有重新赋值给query变量,会导致筛选条件不生效,调整为query = query.Where(filterExpression)即可。
你不想把所有筛选参数塞进单个仓储方法的判断非常合理,这种实现随着筛选条件增多会出现参数爆炸、可复用性极差的问题,下面给两种落地性很强的优化方案:
方案1:使用规格模式(Specification Pattern)封装筛选逻辑
这个模式专门用于解耦筛选条件的定义和执行,你可以把每个独立的筛选规则封装成可复用的规格类,还支持自由拼接组合,适合筛选规则需要多场景复用的中大型项目。
首先定义基础规格抽象类(放在领域层或共享内核中):
public abstract class Specification<T> { public abstract Expression<Func<T, bool>> ToExpression(); public static Specification<T> operator &(Specification<T> left, Specification<T> right) { return new AndSpecification<T>(left, right); } // 可按需扩展Or、Not运算符重载,对应不同的拼接逻辑 } public class AndSpecification<T> : Specification<T> { private readonly Specification<T> _left; private readonly Specification<T> _right; public AndSpecification(Specification<T> left, Specification<T> right) { _left = left; _right = right; } public override Expression<Func<T, bool>> ToExpression() { var leftExpr = _left.ToExpression(); var rightExpr = _right.ToExpression(); // 这里的表达式拼接逻辑和你原有ConcatAnd方法实现一致即可 } // 可选:预定义恒真规格,避免拼接时的null判断 public static readonly Specification<T> All = new TrueSpecification<T>(); }
然后针对比赛的每个独立筛选规则做规格实现(放在领域层):
// 时间区间筛选规格 public class MatchTimeIntervalSpecification : Specification<Match> { private readonly Tuple<DateTime, DateTime> _interval; public MatchTimeIntervalSpecification(Tuple<DateTime, DateTime> interval) => _interval = interval; public override Expression<Func<Match, bool>> ToExpression() { return m => m.StartTime >= _interval.Item1 && (m.StartTime + m.Duration) <= _interval.Item2; } } // 队伍筛选规格 public class MatchBelongsToTeamSpecification : Specification<Match> { private readonly Guid _teamId; public MatchBelongsToTeamSpecification(Guid teamId) => _teamId = teamId; public override Expression<Func<Match, bool>> ToExpression() { return m => m.Team1Id == _teamId || m.Team2Id == _teamId; } } // 球员、位置筛选规格同理实现即可
改造仓储的查询方法,仅需接收规格参数:
// 仓储接口定义(放在应用层/领域层) public interface IMatchRepository { Task<List<Match>> ListAsync(Specification<Match> spec, CancellationToken ct = default); } // 仓储实现(放在基础设施层) public class MatchRepository : IMatchRepository { private readonly DbContext _dbContext; public MatchRepository(DbContext dbContext) => _dbContext = dbContext; public async Task<List<Match>> ListAsync(Specification<Match> spec, CancellationToken ct = default) { var query = _dbContext.Matches .Include(m => m.Team1).ThenInclude(t => t.Players) .Include(m => m.Team2).ThenInclude(t => t.Players); if (spec != null) { query = query.Where(spec.ToExpression()); } return await query.ToListAsync(ct); } }
改造后的Handler代码会非常清爽,仅负责组装规格即可:
public async Task<IEnumerable<MatchDto>> Handle(GetMatchesQuery request, CancellationToken cancellationToken) { var spec = Specification<Match>.All; if (request.TimeInterval != null) spec &= new MatchTimeIntervalSpecification(request.TimeInterval); if (request.Team != null) spec &= new MatchBelongsToTeamSpecification(request.Team.Value); // 其他筛选规格同理拼接 var matches = await _matchRepo.ListAsync(spec, cancellationToken); return _mapper.Map<List<MatchDto>>(matches); }
方案2:基于IQueryable的扩展方法(轻量场景首选)
如果你的业务没有那么复杂,不需要复用太多筛选规则,直接给IQueryable写扩展方法是成本最低的方案,所有扩展方法放在基础设施层即可,无需引入额外的模式概念。
扩展方法定义示例:
public static class MatchQueryExtensions { public static IQueryable<Match> IncludeTeamDetails(this IQueryable<Match> query) { return query .Include(m => m.Team1).ThenInclude(t => t.Players) .Include(m => m.Team2).ThenInclude(t => t.Players); } public static IQueryable<Match> FilterByTimeInterval(this IQueryable<Match> query, Tuple<DateTime, DateTime> interval) { if (interval == null) return query; return query.Where(m => m.StartTime >= interval.Item1 && (m.StartTime + m.Duration) <= interval.Item2); } public static IQueryable<Match> FilterByTeam(this IQueryable<Match> query, Guid? teamId) { if (!teamId.HasValue) return query; return query.Where(m => m.Team1Id == teamId || m.Team2Id == teamId); } // 球员、位置筛选的扩展方法同理实现即可 }
改造后的Handler代码:
public async Task<IEnumerable<MatchDto>> Handle(GetMatchesQuery request, CancellationToken cancellationToken) { var matches = await _dbContext.Matches .IncludeTeamDetails() .FilterByTimeInterval(request.TimeInterval) .FilterByTeam(request.Team) .FilterByPlayer(request.Player) .FilterByLocation(request.Location) .ToListAsync(cancellationToken); return _mapper.Map<List<MatchDto>>(matches); }
架构层面补充建议
- 筛选条件的表达式构建属于基础设施层的实现细节,不要放在应用层的Handler里,Handler仅负责协调业务逻辑,不需要知道具体ORM的筛选实现
- 如果是纯查询场景不需要返回领域实体做业务操作,也可以直接在基础设施层做DTO投影,跳过实体映射步骤,性能会更好
内容的提问来源于stack exchange,提问作者Markus Aurelios
相关产品推荐
相关产品推荐

