使用 FluentValidation 实现数据校验、验重

最近项目里用到了 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 泛型类,其中 UserPassword 泛型是你要验证的类。在验证类的构造方法里写验证规则,代码如下:

    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(); 这句注册你的验证类,如果有多个,就每个都要注册。cfg.RunDefaultMvcValidationAfterFluentValidationExecutes = false; 这句是告诉程序,用 FluentValidation 验证完,不要再使用 Mvc 的验证了。

最后就是在控制器里写处理错误的代码了,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 List GetErrors(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 ModelWithExistId where 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 是没有关系的,这一点容易使人产生困惑,要注意一下。

你可能感兴趣的:(使用 FluentValidation 实现数据校验、验重)