使用具有流畅验证的自定义验证响应

Use custom validation responses with fluent validation

您好,我正在尝试使用 .NET Core 为我的 webApi 获取自定义验证响应。

这里我想要像

这样的响应模型
[{
  ErrorCode:
  ErrorField:
  ErrorMsg:
}]

我有一个验证器 class,目前我们只是检查 ModalState.IsValid 验证错误并将模型状态对象作为 BadRequest 传递。

但是新要求要求我们为每次验证失败设置错误代码。

我的样本验证器Class

public class TestModelValidator :  AbstractValidator<TestModel>{

public TestModelValidator {
   RuleFor(x=> x.Name).NotEmpty().WithErrorCode("1001");
   RuleFor(x=> x.Age).NotEmpty().WithErrorCode("1002");
  }
}

我可以在我的操作中使用类似的东西来获得验证结果

选项 1:

 var validator = new TestModelValidator();
    var result = validator.Validate(inputObj);
    var errorList = result.Error;

并将 ValidationResult 操作到我的自定义 Response 对象。 或者
选项 2:

I can use [CustomizeValidator] attribute and maybe an Interceptors.

但对于 Opt2,我不知道如何从拦截器检索 ValidationResult 到控制器操作。

我只想写一个通用方法,这样我就可以避免在每个控制器操作方法中调用 Opt1 进行验证。

请求指出我正确的资源。

参考此 link 以获得答案:https://github.com/JeremySkinner/FluentValidation/issues/548

解法:

我所做的是创建了一个 basevalidator class,它继承了 IValidatorInterceptor 和 AbstractValidator。在 afterMvcvalidation 方法中,如果验证不成功,我将错误从 validationResult 映射到我的自定义响应对象,并抛出我在异常处理中间件和 return 响应中捕获的自定义异常。

关于控制器获取空对象的序列化问题:

modelstate.IsValid 当 Json 反序列化在模型绑定过程中失败时将设置为 false,并将错误详细信息存储在 ModelState 中。 [这就是我的情况]

同样由于此故障,反序列化不会继续进行,并在控制器方法中获取空对象。

截至目前,我已经通过手动设置序列化 errorcontext.Handled = true 并允许我的 fluentvalidation 捕获无效输入来创建 hack。

https://www.newtonsoft.com/json/help/html/SerializationErrorHandling.htm [在我的请求模型中定义了 OnErrorAttribute]。

我正在寻找更好的解决方案,但目前这个黑客正在做这项工作。

试试这个:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});

在 ActionFilter 中构建 BadResquest 响应后,我使用 fluentvalidation 验证模型 class:

public class ValidateModelStateAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var errors = context.ModelState.Values.Where(v => v.Errors.Count > 0)
                    .SelectMany(v => v.Errors)
                    .Select(v => v.ErrorMessage)
                    .ToList();

            var responseObj = new
            {
                Message = "Bad Request",
                Errors = errors                    
            };

            context.Result = new JsonResult(responseObj)
            {
                StatusCode = 400
            };
        }
    }
}

在StartUp.cs中:

        services.AddMvc(options =>
        {
            options.Filters.Add(typeof(ValidateModelStateAttribute));
        })
        .AddFluentValidation(fvc => fvc.RegisterValidatorsFromAssemblyContaining<Startup>());

        services.Configure<ApiBehaviorOptions>(options =>
        {
            options.SuppressModelStateInvalidFilter = true;
        });

而且效果很好。我希望你觉得它有用

对于我来说,在ASP.NET核心项目

中使用下面的代码比较好
  services.AddMvc().ConfigureApiBehaviorOptions(options =>
  {
    options.InvalidModelStateResponseFactory = c =>
    {
      var errors = string.Join('\n', c.ModelState.Values.Where(v => v.Errors.Count > 0)
        .SelectMany(v => v.Errors)
        .Select(v => v.ErrorMessage));

      return new BadRequestObjectResult(new
      {
        ErrorCode = "Your validation error code",
        Message = errors
      });
    };
  });

还要考虑到您可以使用具体类型代替匿名对象。例如,

     new BadRequestObjectResult(new ValidationErrorViewModel
      {
        ErrorCode = "Your validation error code",
        Message = errors
      });

在 .net 核心中,您可以结合使用 IValidatorInterceptor 将 ValidationResult 复制到 HttpContext.Items,然后使用 ActionFilterAttribute 检查结果和 return如果找到自定义响应。

// If invalid add the ValidationResult to the HttpContext Items.
public class ValidatorInterceptor : IValidatorInterceptor {
    public ValidationResult AfterMvcValidation(ControllerContext controllerContext, ValidationContext validationContext, ValidationResult result) {
        if(!result.IsValid) {
            controllerContext.HttpContext.Items.Add("ValidationResult", result);
        }
        return result;
    }

    public ValidationContext BeforeMvcValidation(ControllerContext controllerContext, ValidationContext validationContext) {
        return validationContext;
    }
}

// Check the HttpContext Items for the ValidationResult and return.
// a custom 400 error if it is found
public class ValidationResultAttribute : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext ctx) {
        if(!ctx.HttpContext.Items.TryGetValue("ValidationResult", out var value)) {
            return;
        }
        if(!(value is ValidationResult vldResult)) {
            return;
        }
        var model = vldResult.Errors.Select(err => new ValidationErrorModel(err)).ToArray();
        ctx.Result = new BadRequestObjectResult(model);
    }
}

// The custom error model now with 'ErrorCode'
public class ValidationErrorModel {
     public string PropertyName { get; }
     public string ErrorMessage { get; }
     public object AttemptedValue { get; }
     public string ErrorCode { get; }

     public ValidationErrorModel(ValidationFailure error) {
         PropertyName = error.PropertyName;
         ErrorMessage = error.ErrorMessage; 
         AttemptedValue = error.AttemptedValue; 
         ErrorCode =  error.ErrorCode;
     }
}

然后在 Startup.cs 你可以像这样注册 ValidatorInterceptorValidationResultAttribute:

public class Startup {
    public void ConfigureServices(IServiceCollection services) {
        services.AddTransient<IValidatorInterceptor, ValidatorInterceptor>();
        services.AddMvc(o => {
            o.Filters.Add<ValidateModelAttribute>()
        });
    }
}

与上面 Alexander 的回答类似,我使用我可以在源代码中找到的原始工厂创建了一个匿名对象,但只是更改了部分以返回自定义 HTTP 响应代码(在我的例子中是 422)。

ApiBehaviorOptionsSetup (Original factory)

services.AddMvcCore()
...
// other builder methods here
...
.ConfigureApiBehaviorOptions(options =>
                {
                    // Replace the built-in ASP.NET InvalidModelStateResponse to use our custom response code
                    options.InvalidModelStateResponseFactory = context =>
                    {
                        var problemDetailsFactory = context.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
                        var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState, statusCode: 422);
                        var result = new UnprocessableEntityObjectResult(problemDetails);
                        result.ContentTypes.Add("application/problem+json");
                        result.ContentTypes.Add("application/problem+xml");
                        return result;
                    };
                });