You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 20:27:04