从 Action Filter 中的 FluentValidation 检索错误代码

Retrieve ErrorCode from FluentValidator in ActionFilter

我正在使用 FluentValidation 库自动验证工作正常的模型 - 但是 - 需要在验证器中使用 WithErrorCode() 方法设置错误代码 (AbstractValidator<T>) .这也可以正常工作,然后问题是从 ASP.NET MVC Core Action Filter 中检索该代码,定义如下:

public class ActionModelValidationAttribute : ActionFilterAttribute
{
    readonly ILogger<ActionModelValidationAttribute> log;
    public ActionModelValidationAttribute (ILogger<ActionModelValidationAttribute> log) => this.log = log;

    public override void OnActionExecuting (ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var routeName = context.RouteData.Values["action"] ?? "unknown";
            log.LogDebug($"model validation failed for {routeName}");

            var errors = context.ModelState.Values.Where(state => state.Errors.Count > 0)
                .SelectMany(errs => errs.Errors)
                .Select(e => new BaseErrorResponse(){
                    Code = 404, // <<-- this is where I would like the code from WithErrorCode()
                    Details = e.Exception?.Message ?? "",
                    Message = e.ErrorMessage,
                    Field = "field"
                }).ToList();

            var response = new ValidationErrorResponseModel()
            {
                Message = "Bad Request",
                Errors = errors                    
            };

            context.Result = new JsonResult(response)
            {
                StatusCode = (int)HttpStatusCode.BadRequest
            };
        }
    }
}

错误的类型是Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateEntry

e的类型是Microsoft.AspNetCore.Mvc.ModelBinding.ModelError

这是我的验证器:

public class ViewModelValidator : AbstractValidator<ViewModel>
{
    public ViewModelValidator() { 
        RuleFor(m => m.DistributorId)
            .NotNull().WithErrorCode("910000")
            .NotEmpty().WithErrorCode("910001");
    }
}

FluentValidation 库似乎无法自行处理此问题。解决方法是在 AbstractValidator<T> 具体实现上实现 IValidatorInterceptor 接口。内存缓存可用于存储唯一的请求 ID,然后可以从操作过滤器中的缓存中检索 ID。将返回一个 ValidationResult 对象,其中包含所有丰富的验证信息。

代码示例如下:

public abstract class BaseModelValidator<T> : AbstractValidator<T>, IValidatorInterceptor
{
    protected readonly IMemoryCache cache;
    protected readonly ILogger<BaseModelValidator<T>> log;
    protected string RequestId { get; set; }

    public BaseModelValidator(IMemoryCache cache, ILogger<BaseModelValidator<T>> log)
    {
        this.cache = cache;
        this.log = log;
    }

    public virtual ValidationContext BeforeMvcValidation(ControllerContext controllerContext, ValidationContext validationContext)
    {
        RequestId = controllerContext.HttpContext.TraceIdentifier;
        return validationContext;
    }

    public virtual ValidationResult AfterMvcValidation(ControllerContext controllerContext, ValidationContext validationContext, ValidationResult result)
    {
        cache.Set(RequestId, result, TimeSpan.FromMinutes(1));
        return result;
    }
}

全局动作过滤器:

public class ActionModelValidationAttribute : ActionFilterAttribute
{
    readonly ILogger<ActionModelValidationAttribute> log;
    readonly IMemoryCache cache;
    public ActionModelValidationAttribute(IMemoryCache cache, ILogger<ActionModelValidationAttribute> log) 
    {
        this.log = log;
        this.cache = cache;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var key = context.HttpContext.TraceIdentifier;
            cache.TryGetValue<ValidationResult>(key, out var result);

            if (result == null) ReturnError(context, key); // impl ReturnError however you like

            cache.Remove(key);
            var count = result.Errors.Count();
            var controllerName = context.RouteData.Values["Controller"] ?? "unknown";
            var routeName = context.RouteData.Values["Action"] ?? "unknown";
            var response = result.AsBaseResponse();
            log.LogDebug($"Model validation failed. {count} errors in model for {controllerName}.{routeName}");

            context.Result = new JsonResult(response)
            {
                StatusCode = (int)HttpStatusCode.BadRequest
            };
        }
    }
}