如何将FluentValidation验证器的HaveUniqueNumber重构为泛型+Lambda
重构FluentValidation唯一验证逻辑为泛型实现
我来帮你搞定这个重构需求——把重复的唯一值验证逻辑抽成通用的泛型方法,这样所有需要类似验证的地方都能复用,不用再写一堆重复的HaveUniqueNumber方法。
核心思路
我们要做的是:
- 封装一个泛型的验证逻辑,支持传入数据库实体类型、动态构建的查询条件Lambda,这样不管是验证
myNumber+parentId,还是其他任意组合的唯一约束,都能适配。 - 结合FluentValidation的链式调用风格,让验证器代码更简洁易读。
第一步:创建泛型扩展方法
先写一个FluentValidation的扩展方法,这样可以直接在RuleFor后面链式调用,比单独写静态工具类更优雅:
using FluentValidation; using Microsoft.EntityFrameworkCore; public static class ValidationExtensions { // 泛型扩展方法:检查指定条件下数据库中是否存在重复值(异步版本) public static IRuleBuilderOptions<TRequest, TValue> IsUniqueAsync<TRequest, TValue, TEntity>( this IRuleBuilder<TRequest, TValue> ruleBuilder, DbContext dbContext, Func<TRequest, Expression<Func<TEntity, bool>>> buildFilter) where TEntity : class { return ruleBuilder.MustAsync(async (request, value, cancellationToken) => { // 根据当前请求模型构建数据库查询条件 var filterExpression = buildFilter(request); // 检查数据库中是否存在符合条件的记录,不存在则验证通过 return !await dbContext.Set<TEntity>().AnyAsync(filterExpression, cancellationToken); }); } // 同步版本(适配仍使用同步数据库操作的项目) public static IRuleBuilderOptions<TRequest, TValue> IsUnique<TRequest, TValue, TEntity>( this IRuleBuilder<TRequest, TValue> ruleBuilder, DbContext dbContext, Func<TRequest, Expression<Func<TEntity, bool>>> buildFilter) where TEntity : class { return ruleBuilder.Must((request, value) => { var filterExpression = buildFilter(request); return !dbContext.Set<TEntity>().Any(filterExpression); }); } }
第二步:改造你的验证器
现在你可以把原来的AddRequestValidator改成这样,注入DbContext后直接使用我们的泛型扩展方法:
public class AddRequestValidator : AbstractValidator<AddRequest> { // 依赖注入DbContext,用来查询数据库 private readonly YourDbContext _dbContext; public AddRequestValidator(YourDbContext dbContext) { _dbContext = dbContext; RuleFor(x => x.myNumber) // 调用泛型扩展方法,指定泛型参数:请求模型、数值类型、数据库实体类型 .IsUniqueAsync<AddRequest, int, YourDbEntity>(_dbContext, // 传入Lambda,从当前请求构建数据库查询条件 request => entity => entity.MyNumber == request.myNumber && entity.ParentId == request.parentId) // 自定义错误消息,用占位符动态显示请求中的值 .WithMessage("myNumber '{0}' already exists for parentId {1}.", x => x.myNumber, x => x.parentId); } }
为什么这么做?
- 完全复用:如果其他验证器(比如
UpdateRequestValidator)需要验证类似的唯一约束,只需要调用同一个IsUniqueAsync方法,传入对应的实体和过滤条件就行,不用再写重复的HaveUniqueNumber。 - 灵活性高:不管是验证单个字段的唯一性,还是多个字段组合的唯一性,只需要修改
buildFilter这个Lambda就行,比如要验证name+tenantId,就写request => entity => entity.Name == request.name && entity.TenantId == request.tenantId。 - 符合最佳实践:通过构造函数注入DbContext,避免硬编码实例,保证代码的可测试性和可维护性。
- 异步友好:用
MustAsync和AnyAsync处理数据库操作,不会阻塞线程,提升应用性能。
额外提示
- 如果需要先验证字段非空,可以在
IsUniqueAsync前面加NotNull()或NotEmpty():RuleFor(x => x.myNumber) .NotNull() .IsUniqueAsync<AddRequest, int, YourDbEntity>(...) - 错误消息模板里的占位符可以根据需求调整,让用户能更清楚地知道哪里出了问题。
内容的提问来源于stack exchange,提问作者user1161137
相关产品推荐
相关产品推荐

