在自定义模型验证器 .NET Core 中格式化错误消息

Formatting error messages in custom model validator .NET Core

在 .NET Core 3.x 应用程序中,我有一个如下所示的模型,其中使用了自定义验证属性 [CustomValidator]CustomValidator 返回的错误消息没有 $. 前缀,而内置 JSON 验证器 returns 带有前缀。我想让它们保持一致(是否始终使用 $. 前缀)。知道如何做到这一点吗?

示例模型:

public class Dto 
{
    public float Price {get; set;}
    [CustomValidator] public int? EntityID {get; set;}
}

其中 [CustomValidator] 是一个自定义验证属性,它执行类似这样的操作

public class CustomValidatorAttribute : ValidationAttribute
{

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {

          var isValid = CheckSomething(...);
          return isValid
                ? ValidationResult.Success
                : new ValidationResult($"Invalid value for Entity");
    }

}

我用

Controller.Action 中验证模型
if (!ModelState.IsValid) return BadRequest(ModelState);

对于输入:

{
  "Price" : "abc"
}

它returns

{
...
    "errors": {
        "$.price": [
            "The JSON value could not be converted to System.Single. Path: $.price | LineNumber: 7 | BytePositionInLine: 21."
        ]
    }
}

而对于输入:

{
  "Price" : 1.0,
  "EntityID": -1,
}

它returns

{
...
    "errors": {
        "EntityID": [
            "Invalid value for Entity"
        ]
    }
}

我希望 errors 始终具有一致的 属性 名称,例如PriceEntityID 而不是 $.priceEntityID

$. 前缀是 JsonException 中包含的 json 路径的一部分。您可以在此处 SystemTextJsonInputFormatter.

默认 json 输入格式化程序的源代码中看到它

因此,从技术上讲,要在将路径传递给方法 ModelState.TryAddModelError 之前对其进行规范化,您当然需要以某种方式修改该路径。那样很复杂。相反,您可以为最终 ProblemDetails 结果配置自定义 InvalidModelStateResponseFactory。尽管通过这种方式,您可能会在性能损失方面做出权衡,尤其是当验证错误频繁发生并且涉及很多错误时。但我认为这在现实中并不常见。这种方法的想法是修改 ModelState,方法是尝试换出所有具有以 $. 为前缀的键的条目,并将它们替换为具有该键前缀的条目。

您可以通过配置 ApiBehaviorOptions 来配置该响应工厂,如下所示:

//inside Startup.ConfigureServices
services.Configure<ApiBehaviorOptions>(o => {
            //we need to call this original factory inside our custom factory
            var originalFactory = o.InvalidModelStateResponseFactory;
            o.InvalidModelStateResponseFactory = context => {   
                //get all the keys prefixed with $. (which should be related to json errors)                 
                var jsonPathKeys = context.ModelState.Keys.Where(e => e.StartsWith("$.")).ToList();
                foreach(var key in jsonPathKeys)
                {
                    var normalizedKey = key.Substring(2);
                    foreach (var error in context.ModelState[key].Errors)
                    {
                        if (error.Exception != null)
                        {
                            context.ModelState.TryAddModelException(normalizedKey, error.Exception);
                        }
                        context.ModelState.TryAddModelError(normalizedKey, error.ErrorMessage);
                    }
                    context.ModelState.Remove(key);
                }                    
                return originalFactory(context);
            };
        });