.NET Core 数据验证:FluentValidation

1,421 阅读3分钟

在 .NET 项目开发中,作为一个比较规范的接口,数据合法性验证是不可或缺的,FluentValidation 是一个目前比较受欢迎的数据验证库,它支持参数定义与验证规则分离,这点在目前很多框架下还是比较重要的,特别是基于接口定义语言自动生成的代码(如:gRPCThrift),使用上与 MVC 中提供的数据验证(System.ComponentModel.DataAnnotations 命名空间中提供的各种数据验证 Attribute,如:RequiredRegularExpressionRange) 的最大区别是 MVC 中验证规则是通过在属性上标记特定的 Attribute。当然还有其他的数据验证方式,甚至也可以完全自己实现,所以在实际项目中选择适合的即可。

下面通过一个简单例子来说明 FluentValidation 的使用,更多请看 FluentValidation 官网介绍,本文关键的部分是介绍如何在项目中优雅并简单的整合这个验证库。

FluentValidation 使用

  1. NuGet 安装 FluentValidation.AspNetCore (Consol/Web Application 均可);

  2. 定义请求对象

    public class TestRequest
    {
      public string Name { get; set; }
    
      public List<string> Ids { get; set; }
    }
    
  3. 定义验证对象:

    public class TestRequestValidator : AbstractValidator<TestRequest>
    {
      public TestRequestValidator()
      {
        RuleFor(_ => _.Name).NotEmpty(); 
        RuleFor(_ => _.Ids).Must(_ => _ != null && _.Count > 0).WithMessage("Ids 不能为空");
      }
    }
    
  4. 实现数据验证

    // 创建一个请求对象,未给属性赋值
    var testRequest = new TestRequest();
    var validator = new TestRequestValidator();
    var result = validator.Validate(testRequest);
    if (!result.IsValid)
    {
      foreach (var error in result.Errors)
      {
        Console.WriteLine($"{error.PropertyName}:{error.ErrorMessage}");
      }
    }
    

result

主要步骤是创建一个基于请求对象的 Validator,在 Validator 中通过 RuleFor 定义一些规则,然后基于验证规则对请求对象的属性值进行校验,如何不合法则通过 Errors 属性返回,一般情况下我们会把这个错误信息返回给接口调用方。

AOP 整合 FluentValidation

通过上面的例子介绍,如果每个接口内都创建当前请求对象的 Validator,然后判断数据是否合法,肯定疯掉。所以我们一般也不会这么玩,这种事情当然是交给 AOP ,如果不了解 AOP 可以 点击这里 。AOP 只是一个概念 ,在 .NET Core 中 AOP 的实现可选择:ActionFilter(MVC)Castle DynamicProxyAspectCoreDora.InterceptionAspect Injector 等,还有一些框架自身已具有拦截器功能,那就可以直接在拦截器内实现数据验证。

这里将使用 Castle DynamicProxy 来介绍整合方法,不过在这之前我们需要先对 FluentValidation 的使用进行封装,提供 InitializeIsValid 两个方法。使用上我们一般会在程序集中定义所有请求对象的 Validator,所以先通过 Initialize 将程序集内的 Validator 初始化到内存中,然后通过请求对象的扩展方法 IsValid 对数据合法性校验,不合法时返回第一个错误信息,具体代码如下:

public static class ValidatorExtension
{
  private static readonly object Locker = new object();
  private static ConcurrentDictionary<string, IValidator> _cacheValidators;

  public static void Initialize(Assembly assembly)
  {
    lock (Locker)
    {
      if (_cacheValidators == null)
      {
        _cacheValidators = new ConcurrentDictionary<string, IValidator>();
        var results = AssemblyScanner.FindValidatorsInAssembly(assembly);
        foreach (var result in results)
        {
          var modelType = result.InterfaceType.GenericTypeArguments[0];
          _cacheValidators.TryAdd(modelType.FullName, (IValidator)Activator.CreateInstance(result.ValidatorType));
        }
      }
    }
  }
  
  public static bool IsValid<T>(this T request, out string msg) where T : class
  {
    msg = string.Empty;

    if (_cacheValidators == null || !_cacheValidators.TryGetValue(request.GetType().FullName, out var validator))
      return true;

    var result = validator.Validate(request);
    if (!result.IsValid)
    {
      // 返回第一个错误信息
      msg = result.Errors[0].ErrorMessage;
      return false;
    }

    return true;
  }
}

项目中安装 Castle.Windsor NuGet 包,实现 Castle.DynamicProxyIInterceptor 接口,以下是部分代码,在方法体执行之前,先通过请求对象的扩展方法 IsValid 进行数据合法性验证,不通过则直接返回错误,合法则继续往下执行,完整代码请 查看这里

public void Intercept(IInvocation invocation)
{
  var request = invocation.Arguments[0];
  var isValid = request.IsValid(out var message);
  if (!isValid)
  {
    var resultType = invocation.Method.ReturnType.GenericTypeArguments[0];
    invocation.ReturnValue = GetParamsErrorValueAsync((dynamic)Activator.CreateInstance(resultType), message);
    return;
  }

  invocation.Proceed();
  invocation.ReturnValue = GetReturnValueAsync((dynamic)invocation.ReturnValue);
}

以上就实现了在拦截器中整合 FluentValidation,避免了接口中单独的一些数据合法性验证代码,使我们更关注业务功能的实现。

参考链接