abp vnext 自定义验证过滤器(ValidateFilter)
首先准备好过滤类:ValidateFilter 和 异常处理类:ExceptionFilter
public class ValidateFilter : IActionFilter, ITransientDependency { /// <summary> /// /// </summary> /// <param name="context"></param> public void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { var result = new ResponseModel(); foreach (var item in context.ModelState.Values) { if (item.ValidationState == ModelValidationState.Invalid) { result.Code = 500; result.Message = item.Errors.FirstOrDefault().ErrorMessage; break; } } context.Result = new JsonResult(result); } } /// <summary> /// /// </summary> /// <param name="context"></param> public void OnActionExecuted(ActionExecutedContext context) { } }
public class ExceptionFilter : ExceptionFilterAttribute { /// <summary> /// /// </summary> /// <param name="context"></param> public override void OnException(ExceptionContext context) { var response = new ResponseModel(false, context.Exception.Message); context.Result = new ContentResult { // 返回状态码设置为200,表示成功 StatusCode = StatusCodes.Status500InternalServerError, // 设置返回格式 ContentType = "application/json;charset=gb2312", Content = JsonConvert.SerializeObject(response) }; context.ExceptionHandled = true; } /// <summary> /// /// </summary> /// <param name="context"></param> /// <returns></returns> public override async Task OnExceptionAsync(ExceptionContext context) { await Task.Run(() => { var response = new ResponseModel(false, context.Exception.Message); context.Result = new ContentResult { // 返回状态码设置为200,表示成功 StatusCode = StatusCodes.Status500InternalServerError, // 设置返回格式 ContentType = "application/json;charset=utf-8", Content = JsonConvert.SerializeObject(response) }; context.ExceptionHandled = true; }); } }
然后在XxxHttpApiHostModule的ConfigureServices方法里面添加
context.Services.AddMvc(options => { options.Filters.Add<ExceptionFilter>(); options.Filters.Add<ValidateFilter>(); });
结果打断点没走这个自定义的ValidateFilter。
TMD,asp.net core 3.1用这种方式妥妥的,到了你abp vnext为啥不行了?你咋这么能呢?
曾尝试过放弃,但我不信解决不了这个问题。然后各种百度、谷歌,QQ群咨询。终于在热心人的帮助下,渐渐明白了个大概。
看源码是必须的。
abp vnext的参数验证走的是AbpValidationActionFilter这个过滤器
public class AbpValidationActionFilter : IAsyncActionFilter, ITransientDependency { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { //TODO: Configuration to disable validation for controllers..? if (!context.ActionDescriptor.IsControllerAction() || !context.ActionDescriptor.HasObjectResult()) { await next(); return; } context.GetRequiredService<IModelStateValidator>().Validate(context.ModelState); await next(); } }
其中红色代码非常重要。看到IModelStateValidator了没?找到它的实现:ModelStateValidator。代码:
public class ModelStateValidator : IModelStateValidator, ITransientDependency { public virtual void Validate(ModelStateDictionary modelState) { var validationResult = new AbpValidationResult(); AddErrors(validationResult, modelState); if (validationResult.Errors.Any()) { throw new AbpValidationException( "ModelState is not valid! See ValidationErrors for details.", validationResult.Errors ); } } public virtual void AddErrors(IAbpValidationResult validationResult, ModelStateDictionary modelState) { if (modelState.IsValid) { return; } foreach (var state in modelState) { foreach (var error in state.Value.Errors) { validationResult.Errors.Add(new ValidationResult(error.ErrorMessage, new[] { state.Key })); } } } }
通过代码我们发现,在Validate方法里面抛出了AbpValidationException异常,最后格式很丑:
{ "error": { "code": null, "message": "Your request is not valid!", "details": "The following errors were detected during validation.\r\n - 姓名不能为空\r\n - 用户名不能为空\r\n", "data": {}, "validationErrors": [ { "message": "姓名不能为空", "members": [ "fullName" ] }, { "message": "用户名不能为空", "members": [ "userName" ] } ] } }
这种错误信息抛给用户无疑非常不友好。因为我在Dto模型上面添加了特性/注解,我就要返回我自己定义的错误信息。你abp vnext不行啊。
说了这么多怎么解决问题呢?说了这么多怎么解决问题呢?说了这么多怎么解决问题呢?
因为 AbpValidationActionFilter 使用 ModelStateValidator 做的处理,那么我将源码中的类:ModelStateValidator,直接拷贝过来放到HttpApi.Host项目里面。
想到就立马验证,跟abp vnext的一模一样:
public class ModelStateValidator : IModelStateValidator, ITransientDependency { public virtual void Validate(ModelStateDictionary modelState) { var validationResult = new AbpValidationResult(); AddErrors(validationResult, modelState); if (validationResult.Errors.Any()) { throw new AbpValidationException( "ModelState is not valid! See ValidationErrors for details.", validationResult.Errors ); } } public virtual void AddErrors(IAbpValidationResult validationResult, ModelStateDictionary modelState) { if (modelState.IsValid) { return; } foreach (var state in modelState) { foreach (var error in state.Value.Errors) { validationResult.Errors.Add(new ValidationResult(error.ErrorMessage, new[] { state.Key })); } } } }
经过断点测试,是走我拷贝来的ModelStateValidator类的。如果我在Validate方法顶部直接return,不做任何逻辑,就会走我自定义的过滤器ValidateFilter了。
到此结束了?还没有,这种方法可以实现,但总感觉没按照abp vnext的套路出牌。
那么我们就按abp vnext的套路来,直接修改ModelStateValidator的Validate方法
public virtual void Validate(ModelStateDictionary modelState) { foreach (var state in modelState) { foreach (var error in state.Value.Errors) { throw new AbpValidationException(error.ErrorMessage); } } }
这样抛出来的异常就是正常的了,比如异常的Message就是:“姓名不能为空”,而不是一大堆的错误信息了。
{ "code": 500, "message": "姓名不能为空", "data": null, "serverTime": "2021-04-28 11:38:58" }
到此结束了?还没有,还有一种更简单的方式,不需要自己处理ModelStateValidator类,而是给ValidateFilter添加顺序
context.Services.AddMvc(options => { options.Filters.Add<ExceptionFilter>(); options.Filters.Add<ValidateFilter>(-1);
});
这样,过滤器ValidateFilter就会优先执行。而且不会走自己封装的ExceptionFilter了。跟AbpValidationActionFilter一点关系都没了。
到此结束了?还没有,还有一种常见的方式,先把AbpValidationActionFilter移除,再添加自定义的Filter,具体类似下面的代码:
var filterMetadata = options.Filters.FirstOrDefault(x => x is ServiceFilterAttribute attribute && attribute.ServiceType.Equals(typeof(AbpValidationActionFilter))); // 移除 AbpValidationActionFilter options.Filters.Remove(filterMetadata); options.Filters.Add<ValidateFilter>();