最近项目里用到了 FluentValidation 对网站用户输入的数据进行了验证,使用起来比较舒服,下面整理一下项目中集成的过程。
需要集成的项目是一个 asp.net core 2.1 版本的项目。第一步,安装 FluentValidation.AspNetCore,VS会自动安装依赖的 FluentValidation、DI 等包。安装完成后,找到你要验证的数据类,比如我这里是一个修改密码的场景,类名是 UserPassword:
public class UserPassword { ////// 用户名 /// public string UserName { get; set; } /// /// 旧密码 /// public string OldPassword { get; set; } /// /// 新密码 /// public string NewPassword { get; set; } /// /// 重复密码 /// public string NewPasswordRe { get; set; } }
找到类后,为这个类写验证规则类,需要继承 AbstractValidator
public class UserPasswordValid : AbstractValidator{ public UserPasswordValid() { CascadeMode = CascadeMode.StopOnFirstFailure; RuleFor(x => x.UserName).NotNull().WithName("用户名"); RuleFor(x => x.OldPassword).NotEmpty().Length(4, 32).WithMessage("旧密码不能为空且长度必须符合规则"); RuleFor(x => x.NewPassword).NotEmpty().Length(4, 32).WithMessage("新密码不能为空且长度必须符合规则") .Must(NewNotEqualsOld).WithMessage("新密码不能跟旧密码一样"); RuleFor(x => x.NewPasswordRe).NotEmpty().WithMessage("重复密码不能为空").Must(ReEqualsNew).WithMessage("重复密码必须跟新密码一样"); } /// /// 判断新旧密码是否一样 /// /// 实体对象 /// 新密码 /// 结果 private bool NewNotEqualsOld(UserPassword model, string newPwd) { return model.OldPassword != newPwd; } /// /// 判断新密码与重复密码是否一样 /// /// /// /// private bool ReEqualsNew(UserPassword model, string newPwdRe) { return model.NewPassword == newPwdRe; } }
FluentValidation 的语法很人性化,初次接触的人大概都能看懂。如 RuleFor(x => x.UserName).NotNull().WithName("用户名");
这句就是 UserName 这个字段不能为 null ,为 null 时,会报 “用户名”不应为 null,WithName 就是给字段指定一个名字。
NotEmpty 就是不能为空,包括 null ;WithMessage 就是不符合条件时的提示。
编写完验证规则,要在 StartUp 里注册一下。在 services.AddMvc 这个子句的后边(注意还是在这句中)加入AddFluentValidation。
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .AddFluentValidation(cfg => { cfg.RegisterValidatorsFromAssemblyContaining(); cfg.RunDefaultMvcValidationAfterFluentValidationExecutes = false; });
其中 cfg.RegisterValidatorsFromAssemblyContaining
最后就是在控制器里写处理错误的代码了,FluentValidation 会把验证的结果写入 ModelState,我们拿 ModelState 来验证就可以。
public IActionResult Index([FromBody]UserPassword userPassword) { if (!ModelState.IsValid) { return Json(new { Success = false, Item = ModelState.GetErrors() }); } else { return Json(new { Success = true }); } }
其中 ModelState.GetErrors() 是我写的一个扩展方法,为了把错误返回给前端,代码如下:
public static class ExtMethods { public static ListGetErrors(this Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary ModelState) { var errors = new List (); foreach (var pair in ModelState) { foreach (var error in pair.Value.Errors) { errors.Add(new ValidationError(pair.Key, error.ErrorMessage)); } } return errors; } public static string ToSingleString(this IEnumerable validations) { return validations.Select(x => x.Message).ToStringDot(); } }
////// 数据验证错误 /// public class ValidationError { public ValidationError(string name, string message) { this.Name = name; this.Message = message; } public string Name { get; set; } public string Message { get; set; } }
代码中的 ToStringDot 方法是我写的扩展方法,功能和 string.Join 一样。我在前端使用的 element-ui 作为前端展示库,效果如下:
对于数据的基本验证这样就可以,但是我们还有一些数据验证逻辑是需要走数据库的,这时就需要我们把验证的方法告诉 FluentValidation。比如在另一个场景中,我需要验证当前数据和数据库现有数据是否重复,验证的方法在一个叫 RoleService 的服务里, RoleService 实现了 IRoleService 接口,并且使用 aspnetcore 自带的微软DI进行注入。 那么我们需要在 Validator 的构造方法里把 Service 注入进来并在 Must 方法里使用这个 Service 进行验证。
////// 角色创建的验证器 /// public class RoleEditDtoValidator : AbstractValidator { public RoleEditDtoValidator(IRoleService roleService) { RuleFor(x => x.RoleId).NotEmpty().WithName("角色编码"); RuleFor(x => x.RoleName).NotEmpty().WithName("角色名称"); RuleFor(x => x).Must(x => roleService.ExistCheck(x.Model)) .When(x => !x.RoleId.NullOrEmpty()) .WithMessage("数据存在重复,请检查!"); } }
然而这样在提示时,不知道提交的数据与数据库里哪条数据发生了重复,所以在提示里要把ID带出来。这个想了半天才想出办法来,代码如下:
////// 角色创建的验证器 /// public class RoleEditDtoValidator : AbstractValidator { public RoleEditDtoValidator(IRoleService roleService) { RuleFor(x => x.RoleId).NotEmpty().WithName("角色编码"); RuleFor(x => x.RoleName).NotEmpty().WithName("角色名称"); RuleFor(x => x.WithExistId()).Must(x => { var rst = roleService.ExistCheck(x.Model, out var existId); x.ExistId = existId; return !rst; }) .When(x => !x.RoleId.NullOrEmpty()) .WithMessage((x, y) => "与 ID 为 " + y.ExistId.ToStringBy("、") + " 的数据存在重复,请检查!"); } }
其中 NullOrEmpty 是我写的一个扩展方法,功能和 string.IsNullOrEmpty 一样, ToStringBy 也是我的扩展方法,和 string.Join 功能一样;这两个扩展方法在 xLiAd.ExtMethods 的包里有,有兴趣的童鞋可以 Nuget 一下。WithExistId 方法是生成一个新的类,功能就是把数据库返回的重复 ID 给带上。代码如下:
public class ModelWithExistIdwhere T : class { readonly T t; public ModelWithExistId(T t) { this.t = t; } public T Model => t; public IEnumerable<int> ExistId { get; set; } } public static class ExtMethods { public static ModelWithExistId WithExistId (this T model) where T : class { return new ModelWithExistId (model); } }
前台提示如下
OK了,这样就差不多了,完美实现需求。
需要注意的是,当你传的参数不符合 MODEL 的规范时 ModelState.IsValid 也会为假,比如你接收 类中的一个字段是 int 型,但你传来个空字符串,ModelState.IsValid 就会报值无效。 但是这个无效和 FluentValidation 是没有关系的,这一点容易使人产生困惑,要注意一下。