Abp vNext异常处理
Abp vNext异常处理
使用Abp vNext 6.0
先来看看官方说的
当满足下面任意一个条件时,AbpExceptionFilter 会处理此异常:
* 当controller action方法返回类型是object result(而不是view result)并有异常抛出时.
* 当一个请求为AJAX(Http请求头中X-Requested-With为XMLHttpRequest)时.
* 当客户端接受的返回类型为application/json(Http请求头中accept 为application/json)时.
如果异常被处理过,则会自动记录日志并将格式化的JSON消息返回给客户端.
- 首先,第一条就有问题,如果我抛出了异常,
throw new Exception("XXX")
那就不能返回object result了,似乎只有BusinessException
、UserFriendlyException
这种apb框架的异常才能自动捕获,所以这个很扯淡,在abp的源码里面似乎用了AbpExceptionHandlingMiddleware
这个中间件来捕获异常来写日志
这个中间件也可以配置,因为我打算重写,所以就懒得去看这个了
services.Configure<AbpExceptionHandlingOptions>(options =>
{
});
- 然后是
application/json
的问题,实际上前端没问题,但是postman默认给的是*/*
,这就不方便测试了 - 还有controller action的问题,比如
DataAnnotations
字段验证的异常,这个在dto里面可以捕获到,但是给属性或函数参数加这个就是抛出异常了 BusinessException
、UserFriendlyException
这种异常也不是真的异常,因为状态码不一定是500,这俩自带的状态码字段是字符串,并不是webapi自带的枚举
throw
简单测试一下异常捕获,postman的Accept要改成application/json才能返回json格式的异常
Exception
public IActionResult Tset()
{
throw new Exception("错误消息");
return Ok();
}
状态码500
{
"error": {
"code": null,
"message": "An internal error occurred during your request!",
"details": null,
"data": {},
"validationErrors": null
}
}
UserFriendlyException
public IActionResult Tset()
{
throw new UserFriendlyException("错误消息");
return Ok();
}
状态码403
{
"error": {
"code": null,
"message": "错误消息",
"details": null,
"data": {},
"validationErrors": null
}
}
BusinessException
public IActionResult Tset()
{
throw new BusinessException("错误消息");
return Ok();
}
状态码403
{
"error": {
"code": "错误消息",
"message": "An internal error occurred during your request!",
"details": null,
"data": {},
"validationErrors": null
}
}
DataAnnotations
定义一个dto
public class CreateOpenIddictApplicationDto
{
[Required]
public string ClientId { get; set; }
[MinLength(6)]
[EmailAddress]
public string ClientSecret { get; set; }
}
加个参数
public IActionResult Tset2([FromBody] CreateOpenIddictApplicationDto input)
{
return Ok();
}
状态码400,这个其实是.net自带的啦
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-35b0d6a5e66350c32f8e6b4aa929d574-59668175c9fea9cd-00",
"errors": {
"ClientId": [
"The ClientId field is required."
],
"ClientSecret": [
"The field ClientSecret must be a string or array type with a minimum length of '6'.",
"The ClientSecret field is not a valid e-mail address."
]
}
}
说实话,用起来属实难受,前端最多就接收一个错误信息,处理500和401就得了,还是重写比较靠谱,这种还是留给mvc玩吧
源码分析
Exception
源码里面是这样调用中间件的
private const string ExceptionHandlingMiddlewareMarker = "_AbpExceptionHandlingMiddleware_Added";
public static IApplicationBuilder UseAbpExceptionHandling(this IApplicationBuilder app)
{
if (app.Properties.ContainsKey(ExceptionHandlingMiddlewareMarker))
{
return app;
}
app.Properties[ExceptionHandlingMiddlewareMarker] = true;
return app.UseMiddleware<AbpExceptionHandlingMiddleware>();
}
public static IApplicationBuilder UseUnitOfWork(this IApplicationBuilder app)
{
return app
.UseAbpExceptionHandling()
.UseMiddleware<AbpUnitOfWorkMiddleware>();
}
所以,如果我们要自定义异常中间件,要在app.UseUnitOfWork()
之前,并且把app.Properties["_AbpExceptionHandlingMiddleware_Added"]
设置为true
那么我们还需要找到AbpExceptionHandlingMiddleware
,至于这个中间件的作用,就是把异常放到响应里面
await httpContext.Response.WriteAsync(
jsonSerializer.Serialize(
new RemoteServiceErrorResponse(
errorInfoConverter.Convert(exception, options =>
{
options.SendExceptionsDetailsToClients = exceptionHandlingOptions.SendExceptionsDetailsToClients;
options.SendStackTraceToClients = exceptionHandlingOptions.SendStackTraceToClients;
})
)
)
);
这里面就是序列号响应对象,所以重要的在于这个errorInfoConverter.Convert
函数
显而易见的,这个转换函数在IExceptionToErrorInfoConverter
里面,源码里面只有一个实现类DefaultExceptionToErrorInfoConverter
public RemoteServiceErrorInfo Convert(Exception exception, Action<AbpExceptionHandlingOptions>? options = null)
{
var exceptionHandlingOptions = CreateDefaultOptions();
options?.Invoke(exceptionHandlingOptions);
var errorInfo = CreateErrorInfoWithoutCode(exception, exceptionHandlingOptions);
if (exception is IHasErrorCode hasErrorCodeException)
{
errorInfo.Code = hasErrorCodeException.Code;
}
return errorInfo;
}
这里面判断了这个IHasErrorCode
接口,显然是BusinessException和UserFriendlyException,并且输出格式与控制台一致
然后是AbpExceptionFilter
和AbpExceptionPageFilter
,这俩是一样的,区别似乎只是webapi和mvc,下面的代码是节选
remoteServiceErrorInfo = exceptionToErrorInfoConverter.Convert(context.Exception, options =>
{
options.SendExceptionsDetailsToClients = exceptionHandlingOptions.SendExceptionsDetailsToClients;
options.SendStackTraceToClients = exceptionHandlingOptions.SendStackTraceToClients;
});
context.HttpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
context.HttpContext.Response.StatusCode = (int)context
.GetRequiredService<IHttpExceptionStatusCodeFinder>()
.GetStatusCode(context.HttpContext, context.Exception);
context.Result = new ObjectResult(new RemoteServiceErrorResponse(remoteServiceErrorInfo));
filter则是在AbpMvcOptionsExtensions
中添加
private static void AddActionFilters(MvcOptions options)
{
options.Filters.AddService(typeof(GlobalFeatureActionFilter));
options.Filters.AddService(typeof(AbpAuditActionFilter));
options.Filters.AddService(typeof(AbpNoContentActionFilter));
options.Filters.AddService(typeof(AbpFeatureActionFilter));
options.Filters.AddService(typeof(AbpValidationActionFilter));
options.Filters.AddService(typeof(AbpUowActionFilter));
options.Filters.AddService(typeof(AbpExceptionFilter));
}
private static void AddPageFilters(MvcOptions options)
{
options.Filters.AddService(typeof(GlobalFeaturePageFilter));
options.Filters.AddService(typeof(AbpExceptionPageFilter));
options.Filters.AddService(typeof(AbpAuditPageFilter));
options.Filters.AddService(typeof(AbpFeaturePageFilter));
options.Filters.AddService(typeof(AbpUowPageFilter));
}
public static void AddAbp(this MvcOptions options, IServiceCollection services)
{
AddConventions(options, services);
AddActionFilters(options);
AddPageFilters(options);
AddModelBinders(options);
AddMetadataProviders(options, services);
AddFormatters(options);
}
这个异常捕获其实是很奇怪的,因为abp在AbpExceptionFilter就处理了异常,并且把异常设置为null,所以异常是传不到AbpExceptionHandlingMiddleware里面的
context.Exception = null; //Handled!
DataAnnotations
首先可以猜测,需要IActionFilter
或IAsyncActionFilter
过滤器,.net自带流程嘛,abp有一个AbpValidationActionFilter
AbpValidationActionFilter里面有一句
context.GetRequiredService<IModelStateValidator>().Validate(context.ModelState);
IModelStateValidator
的实现
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 }));
}
}
}
}
抛出AbpValidationException
异常,然后AbpExceptionHandlingMiddleware
捕获这个异常
emmm,看起来挺真的,但是我关掉.net自带的模型验证后,这个过滤器也是没触发的,实际上没用啦
一开始我认为,AbpValidationActionFilter是IAsyncActionFilter,这个是在IAsyncExceptionFilter之后的,而AbpExceptionFilter是直接捕获异常然后赋值null
,AbpExceptionHandlingMiddleware抓不到这个异常
问题在于这个filter的执行顺序,这个filter是IAsyncActionFilter,如果你写一个IActionFilter,按照.net的执行顺序,应该是执行IAsyncActionFilter,IActionFilter就不会执行了,如果执行IActionFilter,说明IAsyncActionFilter没有触发,这个在实际运行中就是不可靠的,所以要禁用AbpValidationActionFilter
扩展部分
关于数据验证,abp的文档里的验证部分还有提到一个IObjectValidator
,这个接口的实现类ObjectValidator
里有一段
public class ObjectValidator : IObjectValidator, ITransientDependency
{
protected IServiceScopeFactory ServiceScopeFactory { get; }
protected AbpValidationOptions Options { get; }
public ObjectValidator(IOptions<AbpValidationOptions> options, IServiceScopeFactory serviceScopeFactory)
{
ServiceScopeFactory = serviceScopeFactory;
Options = options.Value;
}
public virtual async Task ValidateAsync(object validatingObject, string? name = null, bool allowNull = false)
{
var errors = await GetErrorsAsync(validatingObject, name, allowNull);
if (errors.Any())
{
throw new AbpValidationException(
"Object state is not valid! See ValidationErrors for details.",
errors
);
}
}
public virtual async Task<List<ValidationResult>> GetErrorsAsync(object validatingObject, string? name = null, bool allowNull = false)
{
if (validatingObject == null)
{
if (allowNull)
{
return new List<ValidationResult>(); //TODO: Returning an array would be more performent
}
else
{
return new List<ValidationResult>
{
name == null
? new ValidationResult("Given object is null!")
: new ValidationResult(name + " is null!", new[] {name})
};
}
}
var context = new ObjectValidationContext(validatingObject);
using (var scope = ServiceScopeFactory.CreateScope())
{
foreach (var contributorType in Options.ObjectValidationContributors)
{
var contributor = (IObjectValidationContributor)
scope.ServiceProvider.GetRequiredService(contributorType);
await contributor.AddErrorsAsync(context);
}
}
return context.Errors;
}
}
可以看到ObjectValidator是有抛出AbpValidationException异常的,而IObjectValidationContributor
负责提供错误信息
IObjectValidationContributor有两个实现类FluentObjectValidationContributor
和DataAnnotationObjectValidationContributor
IObjectValidator接口被MethodInvocationValidator
调用,而IMethodInvocationValidator
接口又被ValidationInterceptor
调用,一眼拦截器
public class ValidationInterceptor : AbpInterceptor, ITransientDependency
{
private readonly IMethodInvocationValidator _methodInvocationValidator;
public ValidationInterceptor(IMethodInvocationValidator methodInvocationValidator)
{
_methodInvocationValidator = methodInvocationValidator;
}
public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{
await ValidateAsync(invocation);
await invocation.ProceedAsync();
}
protected virtual async Task ValidateAsync(IAbpMethodInvocation invocation)
{
await _methodInvocationValidator.ValidateAsync(
new MethodInvocationValidationContext(
invocation.TargetObject,
invocation.Method,
invocation.Arguments
)
);
}
}
从源码看起来,我们可以实现IObjectValidationContributor
接口来自定义错误信息,但是这个拦截器的流程其实是有点复杂的,abp的拦截器是Castle DynamicProxy
,按理来说abp是可以处理任何地方的DataAnnotations,但是实际测试我并没有发现这个有用,除了dto外的数据验证都是直接抛异常了,而且官方文档这块也啥都没说,所以这个其实相当不靠谱
abp文档里还有一个IValidatableObject
,可以用dto实现这个接口来自定义验证或返回验证信息,我试了一下,优先级比.net自带的filter低
实现
Exception
大概有两个思路
- 重写中间件
- 实现
IExceptionToErrorInfoConverter
接口
这两个思路其实是一样的,都是改IExceptionToErrorInfoConverter
接口,不过写中间件的话,就是想怎么写就怎么写,实现接口会被返回值RemoteServiceErrorInfo
限制,所以我还是重写中间件吧
其实这里还有个问题,我们是需要一个自定义的异常类的,因为我们需要这个异常类来确定这个异常是否属于要展示给前端的,相当于是只展示手动抛出的异常
abp的异常处理是对全部异常的,什么聚合异常、身份验证异常、数据验证异常,还有本地化错误信息,因为这个中间件是处理响应返回值的,这个异常处理的IExceptionToErrorInfoConverter也不进日志,所以这个只对前端,我们只对指定的异常显示消息,其它的异常统一显示"内部异常"就足够了
首先,先声明一个全局异常类GlobalException
public class GlobalException : Exception
{
public GlobalException() : base()
{
}
public GlobalException(string message) : base(message)
{
}
}
如果要让中间件捕获异常,那就先把filter移除
context.Services.Configure<MvcOptions>(options =>
{
//禁用AbpExceptionFilter
var abpExceptionFilterService = new ServiceFilterAttribute(typeof(AbpExceptionFilter));
options.Filters.Remove(abpExceptionFilterService);
});
异常响应信息模型GlobalExceptionResponseInfoModel
public class GlobalExceptionResponseInfoModel
{
public string Error { get; set; }
public GlobalExceptionResponseInfoModel()
{
this.Error = string.Empty;
}
}
异常响应模型GlobalExceptionResponseModel
public class GlobalExceptionResponseModel
{
/// <summary>
/// 错误信息
/// </summary>
public string Error { get; set; }
public GlobalExceptionResponseModel()
{
this.Error = string.Empty;
}
public GlobalExceptionResponseModel(GlobalExceptionResponseInfoModel exceptionInfo)
{
this.Error = exceptionInfo.Error;
}
}
假装我们有一个中间件的配置类GlobalExceptionHandlerMiddlewareOptions
,这个就是AbpExceptionHandlingOptions
public class GlobalExceptionHandlerMiddlewareOptions
{
public bool SendExceptionsDetailsToClient { get; set; }
public bool SendStackTraceToClient { get; set; }
public GlobalExceptionHandlerMiddlewareOptions()
{
SendExceptionsDetailsToClient = false;
SendStackTraceToClient = true;
}
}
然后就是全局异常处理中间件GlobalExceptionHandlerMiddleware
,这里基本就是照抄AbpExceptionHandlingMiddleware,把最后的响应替换掉就可以了
这个中间件要放到app.UseUnitOfWork()
前面
public class GlobalExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
private readonly Func<object, Task> _clearCacheHeadersDelegate;
private readonly GlobalExceptionHandlerMiddlewareOptions _option;
public GlobalExceptionHandlerMiddleware(RequestDelegate next, IOptions<GlobalExceptionHandlerMiddlewareOptions> options, ILogger<GlobalExceptionHandlerMiddleware> logger)
{
this._next = next;
this._option = options.Value;
this._logger = logger;
this._clearCacheHeadersDelegate = ClearCacheHeaders;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await this._next(httpContext);
}
catch (Exception exception)
{
await HandleAndWrapException(httpContext, exception);
}
}
private async Task HandleAndWrapException(HttpContext httpContext, Exception exception)
{
this._logger.LogException(exception);
await httpContext
.RequestServices
.GetRequiredService<IExceptionNotifier>()
.NotifyAsync(
new ExceptionNotificationContext(exception)
);
if (exception is AbpAuthorizationException)
{
await httpContext.RequestServices.GetRequiredService<IAbpAuthorizationExceptionHandler>()
.HandleAsync(exception.As<AbpAuthorizationException>(), httpContext);
}
else
{
var statusCodeFinder = httpContext.RequestServices.GetRequiredService<IHttpExceptionStatusCodeFinder>();
var jsonSerializer = httpContext.RequestServices.GetRequiredService<IJsonSerializer>();
var exceptionHandlingOptions = httpContext.RequestServices.GetRequiredService<IOptions<GlobalExceptionHandlerMiddlewareOptions>>().Value;
httpContext.Response.Clear();
httpContext.Response.StatusCode = (int)statusCodeFinder.GetStatusCode(httpContext, exception);
httpContext.Response.OnStarting(_clearCacheHeadersDelegate, httpContext.Response);
httpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
httpContext.Response.Headers.Add("Content-Type", "application/json");
await httpContext.Response.WriteAsync(jsonSerializer.Serialize(new GlobalExceptionResponseModel(GlobalExceptionResponseConverter.Convert(exception, options =>
{
options.SendExceptionsDetailsToClient = exceptionHandlingOptions.SendExceptionsDetailsToClient;
options.SendStackTraceToClient = exceptionHandlingOptions.SendStackTraceToClient;
}))));
}
}
private Task ClearCacheHeaders(object state)
{
var response = (HttpResponse)state;
response.Headers[HeaderNames.CacheControl] = "no-cache";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "-1";
response.Headers.Remove(HeaderNames.ETag);
return Task.CompletedTask;
}
}
最后就是全局异常响应转换器GlobalExceptionResponseConverter
public class GlobalExceptionResponseConverter
{
public static GlobalExceptionResponseInfoModel Convert(Exception exception, Action<GlobalExceptionHandlerMiddlewareOptions>? options = null)
{
var exceptionHandlingOptions = GlobalExceptionResponseConverter.CreateDefaultOptions();
options?.Invoke(exceptionHandlingOptions);
var exceptionResponse = GlobalExceptionResponseConverter.CreateExceptionResponseFromException(exception, exceptionHandlingOptions);
return exceptionResponse;
}
/// <summary>
/// 创建异常响应--错误信息
/// </summary>
/// <param name="exception"></param>
/// <param name="options"></param>
/// <returns></returns>
private static GlobalExceptionResponseInfoModel CreateExceptionResponseFromException(Exception exception, GlobalExceptionHandlerMiddlewareOptions options)
{
if (true == options.SendExceptionsDetailsToClient)
{
return GlobalExceptionResponseConverter.CreateDetailExceptionResponseFromException(exception, options.SendStackTraceToClient);
}
GlobalExceptionResponseInfoModel exceptionResponse = new GlobalExceptionResponseInfoModel();
if (exception is GlobalException)
{
exceptionResponse.Error = exception.Message;
}
else
{
exceptionResponse.Error = "内部错误";
}
return exceptionResponse;
}
/// <summary>
/// 创建异常响应--详细错误信息
/// </summary>
/// <param name="exception"></param>
/// <param name="sendStackTraceToClient"></param>
/// <returns></returns>
private static GlobalExceptionResponseInfoModel CreateDetailExceptionResponseFromException(Exception exception, bool sendStackTraceToClient)
{
var detailBuilder = new StringBuilder();
GlobalExceptionResponseConverter.AddExceptionToDetails(exception, detailBuilder, sendStackTraceToClient);
GlobalExceptionResponseInfoModel exceptionResponse = new GlobalExceptionResponseInfoModel();
exceptionResponse.Error = detailBuilder.ToString();
return exceptionResponse;
}
/// <summary>
/// 添加详细异常信息到StringBuilder
/// </summary>
/// <param name="exception"></param>
/// <param name="detailBuilder"></param>
/// <param name="sendStackTraceToClient"></param>
private static void AddExceptionToDetails(Exception exception, StringBuilder detailBuilder, bool sendStackTraceToClient)
{
string globalExceptionMessage = $"{exception.GetType().Name}:内部异常";
if (exception is GlobalException)
{
globalExceptionMessage = $"{exception.GetType().Name}:{exception.Message}";
}
detailBuilder.AppendLine(globalExceptionMessage);
//Details
if (exception is IHasErrorDetails)
{
string details = ((IHasErrorDetails)exception).Details;
if (false == string.IsNullOrWhiteSpace(details))
{
detailBuilder.AppendLine(details);
}
}
//StackTrace
if (true == sendStackTraceToClient)
{
string stackTrace = $"STACK TRACE: {exception.StackTrace}";
detailBuilder.AppendLine(stackTrace);
}
//InnerException
if (null != exception.InnerException)
{
GlobalExceptionResponseConverter.AddExceptionToDetails(exception.InnerException, detailBuilder, sendStackTraceToClient);
}
}
/// <summary>
/// 创建默认全局异常配置
/// </summary>
/// <returns></returns>
private static GlobalExceptionHandlerMiddlewareOptions CreateDefaultOptions()
{
return new GlobalExceptionHandlerMiddlewareOptions();
}
}
试试效果
还是这么个请求
public IActionResult Tset()
{
//throw new Exception("错误消息");
throw new GlobalException("错误消息");
return Ok();
}
Exception,状态码500
{
"error": "内部错误"
}
GlobalException,状态码500
{
"error": "错误消息"
}
成啦
其实这里面有个地方我懒得改了,IHttpExceptionStatusCodeFinder
接口会根据异常类型返回状态码,估计也是可以直接改成500,毕竟abp那些filter都神奇的不会执行,似乎都执行.net自带的filter去了
DataAnnotations
这里跟asp.net core原版一样,把默认的filter禁用,再添加自己写的就行了
先定义一个异常,继承上面的全局异常,因为我是用来验证dto的,所以就叫DTOStateValidationException
public class DTOStateValidationException : GlobalException
{
public DTOStateValidationException() : base()
{
}
public DTOStateValidationException(string message) : base(message)
{
}
}
.net原版的filter里面有日志记录,那么我们也写一个日志,与源码保持一致,但是源码的日志用的是扩展方法,访问权限是internal,所以我们从源码复制一个出来
internal static class DTOStateValidateLoggerExtensions
{
private static readonly Action<ILogger, Exception> _modelStateInvalidFilterExecuting;
static DTOStateValidateLoggerExtensions()
{
DTOStateValidateLoggerExtensions._modelStateInvalidFilterExecuting = LoggerMessage.Define(
LogLevel.Debug,
new EventId(1, "ModelStateInvalidFilterExecuting"),
"The request has model state errors, returning an error response.");
}
public static void ModelStateInvalidFilterExecuting(this ILogger logger) => _modelStateInvalidFilterExecuting(logger, null);
}
因为我们这个filter的设计就是要抛出异常,这个filter只是对异常消息做处理,所以我们可以借鉴一下AbpExceptionFilter和AbpExceptionHandlingMiddleware,这俩的操作基本一致,所以我们的filter也要跟全局异常中间件保持一致,那我们就跟全局异常中间件一样写一个converter,这个converter的操作就是遍历一遍错误信息,然后把第一条错误信息作为异常信息返回
public class DTOStateValidationExceptionConverter
{
/// <summary>
/// 上下文转换为异常
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public static DTOStateValidationException Convert(ActionContext context)
{
string message = DTOStateValidationExceptionConverter.CreateInvalidMessage(context.HttpContext, context.ModelState);
DTOStateValidationException exception = new DTOStateValidationException(message);
return exception;
}
/// <summary>
/// 创建错误信息
/// </summary>
/// <param name="context"></param>
/// <param name="modelState"></param>
/// <returns></returns>
private static string CreateInvalidMessage(HttpContext context, ModelStateDictionary modelState)
{
List<DTOPropertyInvalidModel> errorList = new List<DTOPropertyInvalidModel>();
foreach (var modelPropertyDic in modelState)
{
string propertyName = modelPropertyDic.Key;
ModelStateEntry modelStateEntry = modelPropertyDic.Value;
if (null != modelStateEntry)
{
var propertyErrorList = modelStateEntry.Errors;
if (null != propertyErrorList && propertyErrorList.Count > 0)
{
foreach (var propertyError in propertyErrorList)
{
string propertyErrorMessage = propertyError.ErrorMessage;
if (false == string.IsNullOrWhiteSpace(propertyErrorMessage))
{
DTOPropertyInvalidModel invalidModel = new DTOPropertyInvalidModel();
invalidModel.Name = propertyName;
invalidModel.ErrorMessage = propertyErrorMessage;
errorList.Add(invalidModel);
}
}
}
}
}
string message = DTOStateValidationExceptionConverter.CreateInvalidMessageFromErrorList(errorList);
return message;
}
/// <summary>
/// 根据错误列表创建错误信息
/// </summary>
/// <param name="errorList"></param>
/// <returns></returns>
private static string CreateInvalidMessageFromErrorList(List<DTOPropertyInvalidModel> errorList)
{
string message = "数据验证异常";
if (null != errorList && errorList.Count > 0)
{
DTOPropertyInvalidModel propertyInvalidModel = errorList.FirstOrDefault();
if (null != propertyInvalidModel)
{
message = propertyInvalidModel.ErrorMessage;
}
}
return message;
}
}
这里和中间件一样需要一个model,其实可以用Dictionary将就一下啦
public class DTOPropertyInvalidModel
{
/// <summary>
/// 异常属性名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 异常信息
/// </summary>
public string ErrorMessage { get; set; }
}
然后就是filter,这里的操作就是日志输出一下,然后抛出异常
public class DTOStateValidateFilter : IActionFilter, ITransientDependency
{
private readonly ILogger<DTOStateValidateFilter> _logger;
public DTOStateValidateFilter(ILogger<DTOStateValidateFilter> logger)
{
this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (null == context.Result && false == context.ModelState.IsValid)
{
this._logger.ModelStateInvalidFilterExecuting();
//构建异常DTOStateValidationException
DTOStateValidationException exception = DTOStateValidationExceptionConverter.Convert(context);
throw exception;
}
}
}
最后是配置
//关闭默认模型验证filter
context.Services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
//添加自定义模型验证filter
context.Services.Configure<MvcOptions>(options =>
{
//禁用AbpValidationActionFilter
var abpValidationActionFilterService = new ServiceFilterAttribute(typeof(AbpValidationActionFilter));
options.Filters.Remove(abpValidationActionFilterService);
options.Filters.Add(typeof(DTOStateValidateFilter));
});
试试效果
还是那个dto和请求
public class CreateOpenIddictApplicationDto
{
[Required]
public string ClientId { get; set; }
[MinLength(6)]
[EmailAddress]
public string ClientSecret { get; set; }
}
[HttpPost("test2")]
public IActionResult Tset2([FromBody] CreateOpenIddictApplicationDto input)
{
return Ok();
}
结果符合预期
{
"error": "The ClientId field is required."
}
这里的问题其实蛮多的,dto的属性校验是没有问题啦,确实是DTOStateValidationException,其它地方,比如继承的dto、model、参数之类的,就会报AbpValidationException,说明执行是AbpValidationActionFilter,这就不可靠了
就是说如果执行了AbpValidationActionFilter,就不会执行我们自定义的DTOStateValidateFilter,所以就需要在异常中间件里面再处理AbpValidationException
但是一般我们认为不会返回具体的错误信息给前端,dto的验证可以算作表单提示,其它更底层的信息我们并不想给前端,所以可以忽略这个,启用AbpValidationActionFilter,如果要让DTOStateValidateFilter来提示,就禁用AbpValidationActionFilter
就具体情况而言,建议禁用AbpValidationActionFilter,因为filter是controller的,影响这个的只有dto,其它都是直接抛异常的,所以可以让DTOStateValidateFilter来提示,如果用FluentValidation
的话,那就一定要用中间件处理AbpValidationException了
用FluentValidation就比较纠结了,通常而言,这个就是验证表单的,所以提示是需要的,但是这玩意儿在abp里面又是动态代理的,如果是model用这个,是不能提示给前端的,所以我摆了,不想管AbpValidationException,让前端自己提示吧,但是要重写IHttpExceptionStatusCodeFinder接口,或者配置AbpExceptionHttpStatusCodeOptions,不然这个返回值会变成400,重写接口比较简单,直接拿源码改改就行