[Abp 源码分析]十四、DTO 自动验证
0.简介
在平时开发 API 接口的时候需要对前端传入的参数进行校验之后才能进入业务逻辑进行处理,否则一旦前端传入一些非法/无效数据到 API 当中,轻则导致程序报错,重则导致整个业务流程出现问题。
用过传统 ASP.NET MVC 数据注解的同学应该知道,我们可以通过在 Model 上面指定各种数据特性,然后在前端调用 API 的时候就会根据这些注解来校验 Model 内部的字段是否合法。
1.启动流程
Abp 针对于数据校验分为两个地方进行,第一个是 MVC 的过滤器,也是我们最常使用的。第二个则是借助于 Castle 的拦截器实现的 DTO 数据校验功能,前者只能用于控制器方法,而后者则支持普通方法。
1.1 过滤器注入
在注入 Abp 的时候,通过 AddAbp() 方法内部的 ConfigureAspNetCore()
配置了诸多过滤器。
private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{
// ... 其他代码
//Configure MVC
services.Configure<MvcOptions>(mvcOptions =>
{
mvcOptions.AddAbp(services);
});
// ... 其他代码
}
过滤器注入方法:
internal static class AbpMvcOptionsExtensions
{
public static void AddAbp(this MvcOptions options, IServiceCollection services)
{
// ... 其他代码
AddFilters(options);
// ... 其他代码
}
// ... 其他代码
private static void AddFilters(MvcOptions options)
{
// ... 其他过滤器注入
// 注入参数验证过滤器
options.Filters.AddService(typeof(AbpValidationActionFilter));
// ... 其他过滤器注入
}
// ... 其他代码
}
1.2 拦截器注入
Abp 针对于验证拦截器的注册始于 AbpBootstrapper
类,该基类在之前曾经多次出现过,也就是在用户调用 IServiceCollection.AddAbp<TStartupModule>()
方法的时候会初始化该类的一个实例对象。在该类的构造函数当中,会调用一个 AddInterceptorRegistrars()
方法用于添加各种拦截器的注册类实例。代码如下:
public class AbpBootstrapper : IDisposable
{
private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null)
{
// ... 其他代码
if (!options.DisableAllInterceptors)
{
AddInterceptorRegistrars();
}
}
// ... 其他代码
// 添加各种拦截器
private void AddInterceptorRegistrars()
{
ValidationInterceptorRegistrar.Initialize(IocManager);
AuditingInterceptorRegistrar.Initialize(IocManager);
EntityHistoryInterceptorRegistrar.Initialize(IocManager);
UnitOfWorkRegistrar.Initialize(IocManager);
AuthorizationInterceptorRegistrar.Initialize(IocManager);
}
// ... 其他代码\
}
来到 ValidationInterceptorRegistrar
类型定义当中可以看到,其内部就是通过 Castle 的 IocContainer 来针对每次注入的应用服务应用上参数验证拦截器。
internal static class ValidationInterceptorRegistrar
{
public static void Initialize(IIocManager iocManager)
{
iocManager.IocContainer.Kernel.ComponentRegistered += Kernel_ComponentRegistered;
}
private static void Kernel_ComponentRegistered(string key, IHandler handler)
{
// 判断是否实现了 IApplicationService 接口,如果实现了,则为该对象添加拦截器
if (typeof(IApplicationService).GetTypeInfo().IsAssignableFrom(handler.ComponentModel.Implementation))
{
handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(ValidationInterceptor)));
}
}
}
2.代码分析
从 Abp 库代码当中我们可以知道其拦截器与过滤器是在何时被注入的,下面我们就来具体分析一下他们的处理逻辑。
2.1 过滤器代码分析
Abp 在框架初始化的时候就将 AbpValidationActionFilter
添加到 MVC 的配置当中,其自定义实现的拦截器实现了 IAsyncActionFilter
接口,也就是说当每次接口被调用的时候都会进入该拦截器的内部。
public class AbpValidationActionFilter : IAsyncActionFilter, ITransientDependency
{
// Ioc 解析器,用于解析各种注入的组件
private readonly IIocResolver _iocResolver;
// Abp 针对与 ASP.NET Core 的配置项,主要作用是判断用户是否需要检测控制器方法
private readonly IAbpAspNetCoreConfiguration _configuration;
public AbpValidationActionFilter(IIocResolver iocResolver, IAbpAspNetCoreConfiguration configuration)
{
_iocResolver = iocResolver;
_configuration = configuration;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// ... 处理逻辑
}
}
在内部首先是结合配置项判断用户是否禁用了 MVC Controller 的参数验证功能,禁用了则不进行任何操作。
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 判断是否禁用了控制器检测
if (!_configuration.IsValidationEnabledForControllers || !context.ActionDescriptor.IsControllerAction())
{
await next();
return;
}
// 针对应用服务增加一个验证完成标识
using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Validation))
{
// 解析出方法验证器,传入请求上下文,并且调用这些验证器具体的验证方法
using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>())
{
validator.Object.Initialize(context);
validator.Object.Validate();
}
await next();
}
}
其实我们这里看到有一个 AbpCrossCuttingConcerns.Applying()
方法,那么该方法的作用是什么呢?
在这里我先大体讲述一下该方法的作用,该方法主要是向应用服务对象 (也就是继承了 ApplicationService
类的对象) 内部的 AppliedCrossCuttingConcerns 属性增加一个常量值,在这里也就是 AbpCrossCuttingConcerns.Validation
的值,也就是一个字符串。
那么其作用是什么呢,就是防止重复验证。从启动流程一节我们就已经知道 Abp 框架在启动的时候除了注入过滤器之外,还会注入拦截器进行接口参数验证,当过滤器验证过之后,其实没必要再使用拦截器进行二次验证。
所以在拦截器的 Intercept()
方法内部会有这样一句代码:
public void Intercept(IInvocation invocation)
{
// 判断是否拥有处理过的标识
if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation))
{
invocation.Proceed();
return;
}
// ... 其他代码
}
解释完 AbpCrossCuttingConcerns.Applying()
之后,我们继续往下看代码。
// 解析出方法验证器,传入请求上下文,并且调用这些验证器具体的验证方法
using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>())
{
validator.Object.Initialize(context);
validator.Object.Validate();
}
await next();
这里就比较简单了,过滤器通过 IocResolver
解析出来了一个 MvcActionInvocationValidator
对象,使用该对象来校验具体的参数内容。
2.2 拦截器代码分析
看完过滤器代码之后,其实拦截器代码更加简单。整体逻辑上面与过滤器差不多,只不过针对于拦截器,它是通过一个 MethodInvocationValidator
对象来校验传入的参数内容。
public class ValidationInterceptor : IInterceptor
{
// Ioc 解析器,用于解析各种注入的组件
private readonly IIocResolver _iocResolver;
public ValidationInterceptor(IIocResolver iocResolver)
{
_iocResolver = iocResolver;
}
public void Intercept(IInvocation invocation)
{
// 判断过滤器是否已经处理过
if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation))
{
// 处理过则直接进入具体方法内部,执行业务逻辑
invocation.Proceed();
return;
}
// 解析出方法验证器,传入请求上下文,并且调用这些验证器具体的验证方法
using (var validator = _iocResolver.ResolveAsDisposable<MethodInvocationValidator>())
{
validator.Object.Initialize(invocation.MethodInvocationTarget, invocation.Arguments);
validator.Object.Validate();
}
invocation.Proceed();
}
}
可以看到两个过滤器与拦截器业务逻辑相似,但都是通过验证器来进行处理的,那么验证器又是个什么鬼东西呢?
2.3 参数验证器
验证器即是用来具体执行验证逻辑的工具,从上述代码里面我们可以看到过滤器和拦截器都是通过解析出 MethodInvocationValidator
/MvcActionInvocationValidator
之后调用其验证方法进行验证的。
首先我们来看一下 MVC 的验证器是如何进行处理的,看方法类型的定义,可以看到其继承了一个基类,叫 ActionInvocationValidatorBase
,而这个基类呢,又继承自 MethodInvocationValidator
。
public class MvcActionInvocationValidator : ActionInvocationValidatorBase
{
// ... 其他代码
}
public abstract class ActionInvocationValidatorBase : MethodInvocationValidator
{
// ... 其他代码
}
所以我们分析代码的顺序调整一下,先看一下 MethodInvocationValidator
的内部是如何做处理的吧,这个类型内部还是比较简单的,可能除了有一个递归有点绕之外。
其主要功能就是拿着传递进来的参数值,通过在 Abp 框架启动的时候注入的具体验证器(用户自定义验证器)来递归校验每个参数的值。
/// <summary>
/// 本类用于需要参数验证的方法.
/// </summary>
public class MethodInvocationValidator : ITransientDependency
{
// 最大迭代验证次数
private const int MaxRecursiveParameterValidationDepth = 8;
// 待验证的方法信息
protected MethodInfo Method { get; private set; }
// 传入的参数值
protected object[] ParameterValues { get; private set; }
// 方法参数信息
protected ParameterInfo[] Parameters { get; private set; }
protected List<ValidationResult> ValidationErrors { get; }
protected List<IShouldNormalize> ObjectsToBeNormalized { get; }
private readonly IValidationConfiguration _configuration;
private readonly IIocResolver _iocResolver;
public MethodInvocationValidator(IValidationConfiguration configuration, IIocResolver iocResolver)
{
_configuration = configuration;
_iocResolver = iocResolver;
ValidationErrors = new List<ValidationResult>();
ObjectsToBeNormalized = new List<IShouldNormalize>();
}
// 初始化拦截器参数
public virtual void Initialize(MethodInfo method, object[] parameterValues)
{
Check.NotNull(method, nameof(method));
Check.NotNull(parameterValues, nameof(parameterValues));
Method = method;
ParameterValues = parameterValues;
Parameters = method.GetParameters();
}
// 开始验证参数的有效性
public void Validate()
{
// 检测是否初始化,没有初始化则抛出系统级异常
CheckInitialized();
// 检测方法是否有参数
if (Parameters.IsNullOrEmpty())
{
return;
}
// 检测方法是否为公开方法
if (!Method.IsPublic)
{
return;
}
// 如果没有开启方法参数检测,则直接返回
if (IsValidationDisabled())
{
return;
}
// 如果方法所定义的参数数量与传入的参数值数量匹配不上,则抛出系统级异常
if (Parameters.Length != ParameterValues.Length)
{
throw new Exception("Method parameter count does not match with argument count!");
}
// 遍历方法的参数列表,使用传入的参数值进行校验
for (var i = 0; i < Parameters.Length; i++)
{
ValidateMethodParameter(Parameters[i], ParameterValues[i]);
}
// 如果校验的错误结果集合有任意一条数据,则抛出用户异常,返回给前端展示
if (ValidationErrors.Any())
{
ThrowValidationError();
}
foreach (var objectToBeNormalized in ObjectsToBeNormalized)
{
objectToBeNormalized.Normalize();
}
}
// ... 忽略的代码
// 校验调用方法时传递的参数与参数值
protected virtual void ValidateMethodParameter(ParameterInfo parameterInfo, object parameterValue)
{
// 如果参数值为空的情况下,做一系列特殊判断
if (parameterValue == null)
{
if (!parameterInfo.IsOptional &&
!parameterInfo.IsOut &&
!TypeHelper.IsPrimitiveExtendedIncludingNullable(parameterInfo.ParameterType, includeEnums: true))
{
ValidationErrors.Add(new ValidationResult(parameterInfo.Name + " is null!", new[] { parameterInfo.Name }));
}
return;
}
// 递归校验参数
ValidateObjectRecursively(parameterValue, 1);
}
protected virtual void ValidateObjectRecursively(object validatingObject, int currentDepth)
{
// 验证层级是否超过了最大层级(8)
if (currentDepth > MaxRecursiveParameterValidationDepth)
{
return;
}
// 值是否为空,为空则不继续进行校验
if (validatingObject == null)
{
return;
}
// 判断其类型是否是用户配置的忽略类型,忽略则不进行校验
if (_configuration.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject)))
{
return;
}
// 判断参数类型是否为基本类型
if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObject.GetType()))
{
return;
}
SetValidationErrors(validatingObject);
// 判定参数类型是否实现了 IEnumerabe 接口,如果实现了,则递归遍历校验其内部的元素
if (IsEnumerable(validatingObject))
{
foreach (var item in (IEnumerable) validatingObject)
{
ValidateObjectRecursively(item, currentDepth + 1);
}
}
// 如果实现了标准化接口,则进行标准化操作
if (validatingObject is IShouldNormalize)
{
ObjectsToBeNormalized.Add(validatingObject as IShouldNormalize);
}
// 是否还需要继续递归校验
if (ShouldMakeDeepValidation(validatingObject))
{
var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
foreach (var property in properties)
{
// 如果有禁止校验的特性则忽略
if (property.Attributes.OfType<DisableValidationAttribute>().Any())
{
continue;
}
ValidateObjectRecursively(property.GetValue(validatingObject), currentDepth + 1);
}
}
}
// ... 其他代码
protected virtual bool ShouldValidateUsingValidator(object validatingObject, Type validatorType)
{
return true;
}
// 是否进行深度验证
protected virtual bool ShouldMakeDeepValidation(object validatingObject)
{
// 不需要递归集合对象
if (validatingObject is IEnumerable)
{
return false;
}
var validatingObjectType = validatingObject.GetType();
// 不需要递归基础类型的对象
if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObjectType))
{
return false;
}
return true;
}
// ... 其他代码
}
有朋友可能会奇怪,在方法内部不是通过 IEnumerable
判断之后来进行递归校验么,为什么在最后面还有一个深度验证呢?
这是因为当前对象除了是一个集合的情况之外,还有可能其内部某个对象是另外一个用户所自定义的复杂对象,这个时候就必须要通过深度验证来校验各个参数的值。不过这个递归也是有限度的,通过 MaxRecursiveParameterValidationDepth
来控制这个迭代层数为 8 层。如果不加以限制的话,那么很有可能出现循环引用而产生死循环的情况,或者是层级过深导致接口相应缓慢。
那么在这里执行具体校验操作的则是那些实现了 IMethodParameterValidator
接口的对象,这些对象在 Abp 核心模块(AbpKernelModule
)的预加载的时候被添加到了 Configuration.Validation.Validators
属性当中。
当然用户也可以在自己的模块预加载方法当中增加自己的参数验证器,只要实现该接口即可。
public sealed class AbpKernelModule : AbpModule
{
public override void PreInitialize()
{
// ... 其他代码
// 增加需要忽略的类型
AddIgnoredTypes();
// 增加参数校验器
AddMethodParameterValidators();
}
private void AddMethodParameterValidators()
{
Configuration.Validation.Validators.Add<DataAnnotationsValidator>();
Configuration.Validation.Validators.Add<ValidatableObjectValidator>();
Configuration.Validation.Validators.Add<CustomValidator>();
}
// Abp 默认需要忽略的对象
private void AddIgnoredTypes()
{
var commonIgnoredTypes = new[]
{
typeof(Stream),
typeof(Expression)
};
foreach (var ignoredType in commonIgnoredTypes)
{
Configuration.Auditing.IgnoredTypes.AddIfNotContains(ignoredType);
Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);
}
var validationIgnoredTypes = new[] { typeof(Type) };
foreach (var ignoredType in validationIgnoredTypes)
{
Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);
}
}
}
之后呢,回到之前的校验方法,可以看到在 SetValidationErrors(object validatingObject)
方法里面遍历了之前被注入的验证器集合,然后调用其 Validate()
方法来进行具体的参数校验。
protected virtual void SetValidationErrors(object validatingObject)
{
foreach (var validatorType in _configuration.Validators)
{
if (ShouldValidateUsingValidator(validatingObject, validatorType))
{
using (var validator = _iocResolver.ResolveAsDisposable<IMethodParameterValidator>(validatorType))
{
var validationResults = validator.Object.Validate(validatingObject);
ValidationErrors.AddRange(validationResults);
}
}
}
}
2.4 具体的参数验证器
这里以 Abp 默认实现的 DataAnnotationValidator
类型为例,可以看看他是怎么来根据参数的数据注解来验证参数是否正确的。
public class DataAnnotationsValidator : IMethodParameterValidator
{
public virtual IReadOnlyList<ValidationResult> Validate(object validatingObject)
{
return GetDataAnnotationAttributeErrors(validatingObject);
}
protected virtual List<ValidationResult> GetDataAnnotationAttributeErrors(object validatingObject)
{
var validationErrors = new List<ValidationResult>();
var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
// 获得参数值的所有属性,如果传入的是一个 DTO 对象的话,他内部肯定会有很多属性的
foreach (var property in properties)
{
var validationAttributes = property.Attributes.OfType<ValidationAttribute>().ToArray();
// 没有数据注解特性,跳过当前属性处理
if (validationAttributes.IsNullOrEmpty())
{
continue;
}
// 创建一个错误信息上下文,用户数据注解工具进行校验
var validationContext = new ValidationContext(validatingObject)
{
DisplayName = property.DisplayName,
MemberName = property.Name
};
// 根据特性来校验参数结果
foreach (var attribute in validationAttributes)
{
var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext);
if (result != null)
{
validationErrors.Add(result);
}
}
}
return validationErrors;
}
}
3. 后记
最近工作较忙,可能更新速度不会像原来那么快,不过我尽可能在国庆结束后完成剩余文章,谢谢大家的支持。
4.点此跳转到总目录
[Abp 源码分析]十四、DTO 自动验证的更多相关文章
- C# DateTime的11种构造函数 [Abp 源码分析]十五、自动审计记录 .Net 登陆的时候添加验证码 使用Topshelf开发Windows服务、记录日志 日常杂记——C#验证码 c#_生成图片式验证码 C# 利用SharpZipLib生成压缩包 Sql2012如何将远程服务器数据库及表、表结构、表数据导入本地数据库
C# DateTime的11种构造函数 别的也不多说没直接贴代码 using System; using System.Collections.Generic; using System.Glob ...
- ABP源码分析十四:Entity的设计
IEntity<TPrimaryKey>: 封装了PrimaryKey:Id,这是一个泛型类型 IEntity: 封装了PrimaryKey:Id,这是一个int类型 Entity< ...
- [Abp 源码分析]十五、自动审计记录
0.简介 Abp 框架为我们自带了审计日志功能,审计日志可以方便地查看每次请求接口所耗的时间,能够帮助我们快速定位到某些性能有问题的接口.除此之外,审计日志信息还包含有每次调用接口时客户端请求的参数信 ...
- ABP源码分析十六:DTO的设计
IDTO:空接口,用于标注Dto对象. ComboboxItemDto:用于combobox/list中Item的DTO NameValueDto<T>/NameValueDto:用于na ...
- ABP源码分析十七:DTO 自动校验的实现
对传给Application service对象中的方法的DTO参数,ABP都会在方法真正执行前自动完成validation(根据标注到DTO对象中的validate规则). ABP是如何做到的? 思 ...
- ABP源码分析十九:Auditing
审计跟踪(也叫审计日志)是与安全相关的按照时间顺序的记录,它们提供了活动序列的文档证据,这些活动序列可以在任何时间影响一个特定的操作. AuditInfo:定义如下图中需要被Audit的信息. Aud ...
- ABP源码分析十:Unit Of Work
ABP以AOP的方式实现UnitOfWork功能.通过UnitOfWorkRegistrar将UnitOfWorkInterceptor在某个类被注册到IOCContainner的时候,一并添加到该类 ...
- ABP源码分析十二:本地化
本文逐个分析ABP中涉及到locaization的接口和类,以及相互之间的关系.本地化主要涉及两个方面:一个是语言(Language)的管理,这部分相对简单.另一个是语言对应得本地化资源(Locali ...
- ABP源码分析十五:ABP中的实用扩展方法
类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an attribu ...
随机推荐
- 【运维】在Windows上使用IIS方向代理配置Websocket
最近在做小程序的项目,微信要求所有数据请求通道都要走https或wss.而我们的项目建设如下: api基于C#的MVC webapi开发. websocket基于Nodejs的thinkjs框架开发. ...
- 关于spring aop Advisor排序问题
关于spring aop Advisor排序问题 当我们使用多个Advisor的时候有时候需要排序,这时候可以用注解org.springframework.core.annotation.Order或 ...
- sqlserver2008 批量插入数据
private DataTable GetTableSchema() { DataTable dt = new DataTable(); dt.Columns.AddRange(new DataCol ...
- Django----将列表按照一定的顺序展示
1.要求:按照文章的时间降序排列,并且只展示前5篇文章 2.需要用到:list的切片知识 ###改造view.py中的视图方法 #列表页 def get_article(request): artic ...
- js实现页面重新加载
https://blog.csdn.net/wangjian530/article/details/80596801
- css处理事件透过、点击事件透过
// 执行一些动作... $("#myModal2").css("pointer-events","none"); // 执行一些动作... ...
- java实现点击图片文字验证码
https://www.cnblogs.com/shihaiming/p/7657115.html
- sqlzoo:3
顯示1980年物理學(physics)獲獎者,及1984年化學獎(chemistry)獲得者. select yr,subject,winner from nobel ) ) 查看1980年獲獎者,但 ...
- android BLE Peripheral 做外设模拟设备,供ios、android 连接通讯。
为了能让其它设备可以发现其设备,先启动特定广播.看自己需要什么广播格式. 对于广播可见的mac address: 在调用startAdvertising();时,mac address 就会改变. 并 ...
- MyBatis返回map数据
(1)接口中编写方法 //单行 public Map<String, Object> getEmpReturnMap(Integer id); //多行 @MapKey("id& ...