ASP.NET Core 数据验证:FluentValidation

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

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

FluentValidation 使用

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

  2. 定义请求对象

    1
    2
    3
    4
    5
    6
    public class TestRequest
    {
    public string Name { get; set; }

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建一个请求对象,未给属性赋值
    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 对数据合法性校验,不合法时返回第一个错误信息,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 进行数据合法性验证,不通过则直接返回错误,合法则继续往下执行,完整代码请 查看这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,避免了接口中单独的一些数据合法性验证代码,使我们更关注业务功能的实现。

参考链接

如果对你有帮助就好