在 .NET 项目开发中,作为一个比较规范的接口,数据合法性验证是不可或缺的,FluentValidation 是一个目前比较受欢迎的数据验证库,它支持参数定义与验证规则分离,这点在目前很多框架下还是比较重要的,特别是基于接口定义语言自动生成的代码(如:gRPC
、Thrift
),使用上与 MVC 中提供的数据验证(System.ComponentModel.DataAnnotations
命名空间中提供的各种数据验证 Attribute,如:Required
、RegularExpression
、Range
等) 的最大区别是 MVC 中验证规则是通过在属性上标记特定的 Attribute。当然还有其他的数据验证方式,甚至也可以完全自己实现,所以在实际项目中选择适合的即可。
下面通过一个简单例子来说明 FluentValidation 的使用,更多请看 FluentValidation 官网介绍,本文关键的部分是介绍如何在项目中优雅并简单的整合这个验证库。
FluentValidation 使用
-
NuGet 安装
FluentValidation.AspNetCore
(Consol/Web Application 均可); -
定义请求对象
public class TestRequest { public string Name { get; set; } public List<string> Ids { get; set; } }
-
定义验证对象:
public class TestRequestValidator : AbstractValidator<TestRequest> { public TestRequestValidator() { RuleFor(_ => _.Name).NotEmpty(); RuleFor(_ => _.Ids).Must(_ => _ != null && _.Count > 0).WithMessage("Ids 不能为空"); } }
-
实现数据验证
// 创建一个请求对象,未给属性赋值 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}"); } }
![](https://img.haomeiwen.com/i5378831/e45d8cf7537d5643.png)
主要步骤是创建一个基于请求对象的 Validator
,在 Validator
中通过 RuleFor
定义一些规则,然后基于验证规则对请求对象的属性值进行校验,如何不合法则通过 Errors 属性返回,一般情况下我们会把这个错误信息返回给接口调用方。
AOP 整合 FluentValidation
通过上面的例子介绍,如果每个接口内都创建当前请求对象的 Validator
,然后判断数据是否合法,肯定疯掉。所以我们一般也不会这么玩,这种事情当然是交给 AOP ,如果不了解 AOP 可以 点击这里 。AOP 只是一个概念 ,在 .NET Core 中 AOP 的实现可选择:ActionFilter(MVC)
、Castle DynamicProxy
、AspectCore
、Dora.Interception
、Aspect Injector
等,还有一些框架自身已具有拦截器功能,那就可以直接在拦截器内实现数据验证。
这里将使用 Castle DynamicProxy
来介绍整合方法,不过在这之前我们需要先对 FluentValidation 的使用进行封装,提供 Initialize
、IsValid
两个方法。使用上我们一般会在程序集中定义所有请求对象的 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.DynamicProxy
的 IInterceptor
接口,以下是部分代码,在方法体执行之前,先通过请求对象的扩展方法 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,避免了接口中单独的一些数据合法性验证代码,使我们更关注业务功能的实现。
网友评论