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

请求ASP.NET Web API下CQRS命令的Specification模式实现示例

CQRS命令端的Specification模式适配方案(基于现有查询端实现)

背景说明

我正在开发ASP.NET Web API,采用CQRS模式搭配Mediator、Repository模式+Unit of Work,目前查询端的Specification模式已实现,代码如下:

基础Specification抽象类

public abstract class BaseSpecification<T> : ISpecification<T>
{
    protected BaseSpecification(Expression<Func<T, bool>> criteria)
    {
        Criteria = criteria;
    }
    protected BaseSpecification()
    {
    }
    public Expression<Func<T, bool>> Criteria { get; }
    public List<Expression<Func<T, object>>> Includes { get; } = new();

    protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)
    {
        Includes.Add(includeExpression);
    }
}

关联查询Specification示例

public MediaIncludePeopleAndGenres(int mediaId) 
        : base(x => x.MediaId == mediaId)
{
    AddInclude(x => x.MediaGenres);
    AddInclude(x => x.MediaPeople);
}

EF上下文扩展方法

public static IQueryable<T> Specify<T>(this IQueryable<T> query, ISpecification<T> spec) where T : class
{
    var resultWithIncludes = spec
        .Includes
        .Aggregate(query, (current, include) => current.Include(include));

    return resultWithIncludes.Where(spec.Criteria);
}

查询仓储方法

public Task<Media?> GetMediaByIdAsync(int id)
{
    return _context.Media
        .Specify(new MediaIncludePeopleAndGenres(id))
        .FirstOrDefaultAsync();
}

查询端实现符合预期,但不清楚命令端(Post、Put等操作)的Specification应包含哪些功能,希望获取契合现有实现的示例。当前命令端调用方式如下:

基础命令仓储方法

public void AddSingleMedia(Media media)
{
     _context.Media.Add(media);
}

批量操作仓储方法(使用EF扩展工具)

public Task BulkInsertMediaAsync(List<Media> mediaList)
{
    return _context.Media.BulkInsertAsync(mediaList, options => options.IncludeGraph = true);
}

命令端Specification适配方案

命令端的Specification核心是封装命令场景下的复用逻辑,主要覆盖三类场景:实体定位与关联加载、业务规则校验、批量操作筛选。以下是结合现有实现的示例:

1. 扩展基础Specification类,适配命令场景

首先扩展现有BaseSpecification<T>,增加业务规则校验相关的属性和方法,保持与查询端逻辑兼容:

public abstract class BaseSpecification<T> : ISpecification<T>
{
    protected BaseSpecification(Expression<Func<T, bool>> criteria)
    {
        Criteria = criteria;
    }
    protected BaseSpecification()
    {
    }
    public Expression<Func<T, bool>> Criteria { get; }
    public List<Expression<Func<T, object>>> Includes { get; } = new();
    // 新增:命令端业务规则集合
    public List<Func<T, bool>> BusinessRules { get; } = new();

    protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)
    {
        Includes.Add(includeExpression);
    }

    // 新增:添加业务规则的方法
    protected virtual void AddBusinessRule(Func<T, bool> rule)
    {
        BusinessRules.Add(rule);
    }

    // 新增:执行规则校验的方法
    public virtual IEnumerable<string> Validate(T entity)
    {
        var errors = new List<string>();
        foreach (var rule in BusinessRules)
        {
            if (!rule(entity))
            {
                errors.Add(GetRuleErrorMessage(rule));
            }
        }
        return errors;
    }

    // 可重写:返回规则对应的错误信息
    protected virtual string GetRuleErrorMessage(Func<T, bool> rule)
    {
        return "业务规则校验失败";
    }
}

2. 更新操作:实体定位与关联加载Specification

用于更新操作时,加载目标实体及其关联数据,方便后续的字段更新和关联处理:

public class MediaUpdateSpecification : BaseSpecification<Media>
{
    public MediaUpdateSpecification(int mediaId) 
        : base(x => x.MediaId == mediaId)
    {
        // 加载关联分类,支持更新时同步修改关联数据
        AddInclude(x => x.MediaGenres);
    }
}

对应的仓储方法:

public async Task<Media?> GetMediaForUpdateAsync(int id)
{
    return await _context.Media
        .Specify(new MediaUpdateSpecification(id))
        .FirstOrDefaultAsync();
}

public void UpdateMedia(Media media)
{
    _context.Media.Update(media);
}

在命令处理器中使用:

public class UpdateMediaCommandHandler : IRequestHandler<UpdateMediaCommand, bool>
{
    private readonly IRepository<Media> _repository;
    private readonly IUnitOfWork _unitOfWork;

    public UpdateMediaCommandHandler(IRepository<Media> repository, IUnitOfWork unitOfWork)
    {
        _repository = repository;
        _unitOfWork = unitOfWork;
    }

    public async Task<bool> Handle(UpdateMediaCommand request, CancellationToken cancellationToken)
    {
        // 通过Specification获取待更新的实体及关联数据
        var media = await _repository.GetMediaForUpdateAsync(request.MediaId);
        if (media == null)
            return false;

        // 映射更新字段
        media.Title = request.Title;
        // 同步更新关联的MediaGenres...

        _repository.UpdateMedia(media);
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        return true;
    }
}

3. 创建操作:业务规则校验Specification

封装创建实体时的业务规则,避免在命令处理器中硬编码校验逻辑:

public class MediaCreateSpecification : BaseSpecification<Media>
{
    private readonly AppDbContext _context;

    public MediaCreateSpecification(AppDbContext context)
    {
        _context = context;
        // 添加名称唯一规则
        AddBusinessRule(media => !_context.Media.Any(m => m.Title == media.Title));
        // 添加媒体类型合法性规则
        AddBusinessRule(media => Enum.IsDefined(typeof(MediaType), media.Type));
    }

    protected override string GetRuleErrorMessage(Func<Media, bool> rule)
    {
        if (rule.ToString().Contains("Title"))
            return "媒体名称已存在";
        if (rule.ToString().Contains("Type"))
            return "媒体类型不合法";
        return base.GetRuleErrorMessage(rule);
    }
}

在命令处理器中使用:

public class CreateMediaCommandHandler : IRequestHandler<CreateMediaCommand, int>
{
    private readonly IRepository<Media> _repository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly MediaCreateSpecification _createSpec;

    public CreateMediaCommandHandler(IRepository<Media> repository, IUnitOfWork unitOfWork, MediaCreateSpecification createSpec)
    {
        _repository = repository;
        _unitOfWork = unitOfWork;
        _createSpec = createSpec;
    }

    public async Task<int> Handle(CreateMediaCommand request, CancellationToken cancellationToken)
    {
        var media = new Media
        {
            Title = request.Title,
            Type = request.Type,
            // 填充其他字段
        };

        // 用Specification执行业务规则校验
        var errors = _createSpec.Validate(media);
        if (errors.Any())
            throw new ValidationException(string.Join("; ", errors));

        _repository.AddSingleMedia(media);
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        return media.MediaId;
    }
}

4. 批量操作:筛选条件Specification

用于批量更新/删除时,封装目标实体的筛选规则:

public class MediaBulkUpdateSpecification : BaseSpecification<Media>
{
    public MediaBulkUpdateSpecification(MediaType targetType) 
        : base(x => x.Type == targetType)
    {
    }
}

对应的仓储批量更新方法:

public async Task BulkUpdateMediaByTypeAsync(MediaType targetType, Action<Media> updateAction)
{
    // 用Specification筛选目标实体
    var mediaList = await _context.Media
        .Specify(new MediaBulkUpdateSpecification(targetType))
        .ToListAsync();

    // 执行自定义更新逻辑
    foreach (var media in mediaList)
    {
        updateAction(media);
    }

    // 调用批量扩展工具提交更新
    await _context.Media.BulkUpdateAsync(mediaList);
}

总结

命令端的Specification延续了查询端"封装复用逻辑"的核心思想,主要解决三类问题:

  • 统一管理更新/删除操作中实体的查询条件与关联加载规则
  • 集中封装创建/更新时的业务规则校验逻辑
  • 规范批量操作的实体筛选条件

这种实现方式完全兼容现有查询端的代码结构,无需大幅重构即可落地。

内容的提问来源于stack exchange,提问作者Ruby Rain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 04:45:03