本篇实使用FluentValidation时自动注册以及在注册后自动验证,无须在接口中添加验证代码的功能。
1.相应开发环境
- .net core 3.1
- Nuget包 FluentValidation 10.0.0
2.原校验过程
以下以Dmeo为例进行校验的过程,定义一个获取用户信息的接口,用FluentValidation对入参进行校验。
2.1 定义入参
定义一个入参UserRequest,定义4个属性,分别为姓名,性别,电话和地址。
/// <summary>
/// 用户入参请求
/// </summary>
public class UserRequest
{
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 性别
/// </summary>
public string Sex { get; set; }
/// <summary>
/// 电话
/// </summary>
public string Phone { get; set; }
/// <summary>
/// 地址
/// </summary>
public string Address { get; set; }
}
2.2 定义校验类
根据入参UserRequest定义一个校验类UserValidator来对参数模型进行校验。
public class UserValidator : AbstractValidator<UserRequest>
{
public UserValidator()
{
//遇到第一个失败即停止
CascadeMode = CascadeMode.Stop;
//校验姓名不能为空
RuleFor(i => i.Name).NotEmpty().WithMessage("姓名不能为空");
}
}
2.3 在接口中实现校验
在接口中需要先实例化一个校验类,然后将入参代入到校验类的Validate方法中进行校验,再对校验类验证后的接口进行处理。
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
/// <summary>
///
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("Get")]
public ActionResult GetUser([FromBody] UserRequest input)
{
//实例化校验类
var userValidator = new UserValidator();
//验证
var validResult = userValidator.Validate(input);
//处理验证结果
if(!validResult.IsValid)
{
return new JsonResult(new { Code = 500, Msg = validResult.Errors[0].ErrorMessage });
}
return new JsonResult("已获取用户信息");
}
}
3. 现校验过程
原校验过程中对接口而言,需要每次初始化校验类,且对校验类结果进行处理。
现改后的思路为注入服务的方式对入参进行校验,在模型绑定后用过滤器对参数进行校验,在校验失败时直接返回,不进入接口内部的逻辑。
3.1 定义校验接口
定义一个服务IValidatorService,实现验证的功能
public interface IValidatorService
{
/// <summary>
/// 默认校验
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="message"></param>
/// <returns></returns>
bool Valid<T>(T value, out string message);
/// <summary>
/// 按规则校验
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="rule"></param>
/// <param name="message"></param>
/// <returns></returns>
bool Valid<T>(T value, string rule, out string message);
}
3.2 定义实现的具体校验
实现具体的IValidatorService,对校验类进行注册并验证。ValidatorService内部使用一个RegisterValidator的方法对校验类进行注册
/// <summary>
/// 自动注册服务
/// </summary>
private void RegisterValidator()
{
var assemblyConfig = new List<Assembly>();
//集中放置校验类时
assemblyConfig.Add(Assembly.GetAssembly(typeof(UserValidator)));
////分开放置校验类时
//foreach (string filePath in Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "CustomFluentValidationDemo.dll"))
//{
// assemblyConfig.Add(Assembly.LoadFrom(filePath));
//}
foreach (var assembly in assemblyConfig)
{
foreach (var i in assembly.GetTypes())
{
if (i.IsInterface) continue;
foreach (var type in i.GetInterfaces())
{
if (type.Name == "IValidator`1")
{
_validatorSet[type.GenericTypeArguments[0]] = (IValidator)Activator.CreateInstance(i);
}
}
}
}
}
ValidatorService内部使用Valid的方法对模型校验进行验证并返回错误结果
/// <summary>
/// 验证
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="message"></param>
/// <returns></returns>
public bool Valid<T>(T value, out string message)
{
message = string.Empty;
Type type = value.GetType();
string typeName = type.ToString();
if (!_validatorSet.ContainsKey(type))
{
message = $"未找到{value}相对应的验证类";
return false;
}
//验证途中如果没有 则新加入验证方法
if (!_validaBehaviorSet.ContainsKey(typeName))
{
_validaBehaviorSet.TryAdd(typeName, _validatorSet[type].Validate);
}
var context = new ValidationContext<T>(value);
ValidationResult result = _validaBehaviorSet[typeName](context);
if (result.IsValid) return true;
message = result.Errors?[0].ErrorMessage;
return false;
}
3.3 定义过滤器
定义一个参数验证的过滤器,在模型绑定后对参数进行校验,校验成功则进入接口,校验失败则返回错误。
/// <summary>
/// 参数校验
/// </summary>
public class ParamValidateAttribute : ActionFilterAttribute
{
/// <summary>
/// 规则名
/// </summary>
private readonly string _ruleName;
public ParamValidateAttribute(string ruleName = null)
{
_ruleName = ruleName;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var validatorService = context.HttpContext.RequestServices.GetService(typeof(IValidatorService)) as IValidatorService;
string message = string.Empty;
//依次对参数进行校验
foreach (var argument in context.ActionArguments)
{
if (!validatorService.Valid(argument.Value, _ruleName, out message))
{
//可根据项目结构自行定义返参
var result = new
{
Code = 500,
Msg = message
};
context.Result = new Microsoft.AspNetCore.Mvc.JsonResult(result);
break;
}
}
base.OnActionExecuting(context);
}
}
3.4 注册服务
向.net core的框架注入定义的服务,在startup的ConfigureServices方法中添加以下的服务注册方式。这步一定要用AddSingleton的模式进行注册,不能用Scope或者Transient的方式进行,需要将ValidatorService变成一个单例,其中的_validaBehaviorSet和_validaRuleBehaviorSet不会置空,能够将每次调用的校验保存下来,实现复用。
services.AddSingleton<IValidatorService, ValidatorService>();
3.5 使用服务
为了区分和之前定义的接口,新增两个接口,一个是删除用户和更新用户的接口,删除用户用的是默认配置的校验类,而更新接口使用的是校验类中另行配置的规则集,共两种方式。
[HttpPost("Delete"), ParamValidate()]
public ActionResult DeleteUser([FromBody] UserRequest input)
{
return new JsonResult("已删除用户信息");
}
[HttpPost("Update"), ParamValidate("Update")]
public ActionResult UpdateUser([FromBody] UserRequest input)
{
return new JsonResult("已更新用户信息");
}
在校验类中需要添加的部分代码
public class UserValidator : AbstractValidator<UserRequest>
{
public UserValidator()
{
//遇到第一个失败即停止
CascadeMode = CascadeMode.Stop;
//校验姓名不能为空
RuleFor(i => i.Name).NotEmpty().WithMessage("姓名不能为空");
RuleSet("Update", () =>
{
RuleFor(i => i.Name).NotEmpty().WithMessage("姓名不能为空");
RuleFor(i => i.Sex).NotEmpty().WithMessage("性别不能为空");
});
}
}
3.6 调用接口
使用postman对上述的两个接口进行调用,则接口结合FluentValidation正常运行。
调用接口api/user/delete时,缺失name,则会根据规则产生如下错误
{
"code": 500,
"msg": "姓名不能为空"
}
调用接口api/user/update时,缺失sex,则会根据规则产生如下错误
{
"code": 500,
"msg": "性别不能为空"
}
4. 总结
改变原来的方式后,少写了部分重复代码,更加方便。但同样要注意,避免出现未注册的情况出现。
FluentValidation的文档地址:https://docs.fluentvalidation.net/en/latest/start.html
demo地址:https://github.com/thePengLong/CustomFluentValidationDemo
如有问题,恳请指出。