请求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
相关产品推荐
相关产品推荐

