【问题标题】:ModelState not show all errorsModelState 不显示所有错误
【发布时间】:2020-01-25 11:05:36
【问题描述】:

我有一个带有 ActionFilter 的 ASP NET Core 2.1 API(自动 ModelState 验证被抑制),并且当出现绑定错误时 - 例如要绑定到 guid 的无效字符串 - 模型状态仅包含来自绑定的错误,但不包含其他错误 - 需要属性或 MaxLength 等。这是预期的吗?还有更重要的问题:有没有办法在一次旅行中获取所有模型状态错误?

我的操作过滤器(全局):

public void OnActionExecuting(ActionExecutingContext context)
{
    if (!context.ModelState.IsValid)
    {
        context.Result = new BadRequestObjectResult(context.ModelState);
    }
}

绑定模型:

public class SkillBindDto
{
    [Required(ErrorMessage = ValidationMessages.FieldRequired)]
    [MinLength(1, ErrorMessage = ValidationMessages.FieldInvalidMinLength)]
    public string Name { get; set; }

    public string Info { get; set; }

    [Required(ErrorMessage = ValidationMessages.FieldRequired)]
    public Guid SectionId { get; set; }

    public string[] Tags { get; set; }
}

控制器中的Action方法

[HttpPost()]
public async Task<ActionResult<IReadOnlyCollection<SkillDto>>> Create([FromBody]ICollection<SkillBindDto> skills, CancellationToken cancellationToken)
{
    List<SkillDto> result = await _skillService.CreateSkillsAsync(skills, cancellationToken);

    return result;
}

还有两个例子: 当请求的正文是:

[
    {
        SectionId : "0c2d3928-aff2-44da-blaaah-blaaah", - this is invalid guid
        Name : "",
        Info : "Test Info",
        Tags : ["tag 1", "tag 2"]
    },
        {
        SectionId : "0c2d3928-aff2-44da-blaaah-blaaah", - this is invalid guid
        Name : "",
        Info : "Test Info 2",
        Tags : ["tag 3", "tag 2"]
    }
]

我收到了这样的回复:

{
    "[0].SectionId": [
        "Error converting value \"0c2d3928-aff2-44da-blaaah-blaaah\" to type 'System.Guid'. Path '[0].SectionId', line 3, position 51."
    ],
    "[1].SectionId": [
        "Error converting value \"0c2d3928-aff2-44da-blaaah-blaaah\" to type 'System.Guid'. Path '[1].SectionId', line 9, position 51."
    ]
}

当 Section Id guid 有效时:

[
    {
        SectionId : "0c2d3928-aff2-44da-5d98-08d727c1a8b0",
        Name : "",
        Info : "Test Info",
        Tags : ["tag 1", "tag 2"]
    },
    {
        SectionId : "0c2d3928-aff2-44da-5d98-08d727c1a8b0",
        Name : "",
        Info : "Test Info",
        Tags : ["tag 3", "tag 2"]
    }
]

结果是:

{
    "[0].Name": [
        "Field Name is not provided but it is required",
        "Field Name is under minimum length. Lenght must be not less than 1 character(s)"
    ],
    "[1].Name": [
        "Field Name is not provided but it is required",
        "Field Name is under minimum length. Lenght must be not less than 1 character(s)"
    ]
}

【问题讨论】:

    标签: c# asp.net-core


    【解决方案1】:

    当存在绑定错误时 - 例如绑定到 guid 的字符串无效 - 模型状态仅包含来自绑定的错误,但不包含其他错误 - 需要属性或 MaxLength 等。这是预料之中的事情吗?

    事实并非如此。真正的原因是您正在处理 JSON 有效负载。 json payload 必须先反序列化ICollection&lt;SkillBindDto&gt;,然后validator 才能对其进行验证。

    当使用无效的GUID 属性处理您的负载时,反序列化过早失败,因此将没有进一步的绑定/验证 其他属性。

    有没有办法在一次行程中获取所有模型状态错误?

    正如我上面所说,问题的发生是因为 JSON Deserailziation 失败。

    如果您想使用 JSON 格式,请告诉 MVC 如何处理无效属性。例如,创建一个自定义JsonConverter

    public class MyCustomConverter : JsonConverter
    {
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var dt= reader.Value;
            ... // because the processing rules depends on your business, 
            ... //     I can't write all the codes here.
            ... // e.g.
            ... //     create a null(object) or default(value) if invalid
        }
    }
    

    轻松解决

    作为一个更简单的walkaround,您可以发送表单有效负载而不是application/json

    form payload 被编码为以“&”分隔的键值对,键和值之间有一个“=”,看起来像查询字符串。当您在过去提交 HTML &lt;form /&gt; 时,实际上是在发送表单有效负载。

    首先,删除操作中的[FromBody] 属性:

    [HttpPost()] 公共异步任务>> Create([FromBody]ICollection 技能,CancellationToken cancelToken) { ... }

    然后以application/x-www-form-urlencoded的格式发送payload。您可以使用new FormData(formElementId) 来构造这样的表单有效负载。我创建了一个将 JSON 映射到表单数据的辅助函数:

    function createFormPayload(name,o){
        var payload = {};
        function _objectNotNull(value){
            return value !== null && typeof value === "object";
        }
        function _create(prefix,obj) {
            for(var prop in obj) {
                if (obj.hasOwnProperty(prop)) {
                    var key = "";
                    if(prefix){
                        key = isNaN(prop)? key = prefix + "." + prop : key = prefix + ".[" + prop + "]";
                    }else{
                        key = isNaN(prop)? key = prop : key = "[" + prop + "]";
                    }
                    var value = obj[prop];
                    if(_objectNotNull(value)) 
                        _create(key, value); 
                    else 
                        payload[key]=value;
                }
            }
        };
        _create(name,o);
        return payload;
    }
    

    现在我们可以将skills 发送到表单数据对象:

    var skills= [
        {
            "SectionId" : "0c2d3928-aff2-44da-blaaah-blaaah",
            "Name" : "",
            "Info" : "Test Info",
            "Tags" : ["tag 1", "tag 2"]
        }, {
            "SectionId" : "0c2d3928-aff2-44da-blaaah-blaaah",
            "Name" : "",
            "Info" : "Test Info 2",
            "Tags" : ["tag 3", "tag 2"]
        }
    ];
    
    var data = createFormPayload("",skills) ;
    $.ajax({
        type: "POST",
        url: "/xxx/create",
        data: data,
        success: function(r){
            console.log(r);
        },
    });
    

    演示

    发送上述技能时,实际有效载荷为:

    POST https://localhost:5001/Home/Create
    Content-Type: application/x-www-form-urlencoded
    
    [0].SectionId=0c2d3928-aff2-44da-blaaah-blaaah&[0].Name=&[0].Info=Test Info&[0].Tags[1]=tag1&[0].Tags[2]=tag2&[1].SectionId=0c2d3928-aff2-44da-blaaah-blaaah&[1].Name=&[1].Info=TestInfo2&[1].Tags[1]=tag3&[1].Tags[2]=tag 2
    

    然后您将收到描述所有错误的响应:

    【讨论】:

    • 我的 API 被我无法控制的其他应用程序使用。因此,更改有效负载不是一种选择。但是自定义 JsonConverter 对我来说是一个很好的建议。
    【解决方案2】:

    我将发布解决我的问题的代码。它基于 itminus 答案。 所以最后我为Guids使用了一个自定义的JsonConverter,效果很好。

    自定义 JsonConverter - 如果无法将字符串从有效负载转换为 Guid,则将 corespondent 字段设置为空 Guid 值,因此有效负载被序​​列化并可以继续进行其他验证:

    public sealed class JsonConverterDefaultGuid : JsonConverter<Guid>
    {
        public override Guid ReadJson(JsonReader reader, Type objectType, Guid existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            string value = (string)reader.Value;
            return Guid.TryParse(value, out var result) ? result : Guid.Empty; 
        }
    
        public override void WriteJson(JsonWriter writer, Guid value, JsonSerializer serializer)
        {
            writer.WriteValue(value.ToString());
        }
    }
    

    下一步 - 创建自定义验证属性,检查装饰属性是否具有默认值并添加模型状态错误:

    public class NotDefaultAttribute : ValidationAttribute
    {
        public const string DefaultErrorMessage = "The {0} field must not have default value";
    
        public NotDefaultAttribute() : base(DefaultErrorMessage)
        {
    
        }
    
        public NotDefaultAttribute(string errorMessage) : base(errorMessage)
        {
    
        }
    
        public override bool IsValid(object value)
        {
            return !value.Equals(value.GetDefaultValue());
        }
    }
    

    该属性使用扩展方法为任何对象生成默认值:

    public static object GetDefaultValue(this object obj)
    {
          var objType = obj.GetType();
    
          if (objType.IsValueType)
              return Activator.CreateInstance(objType);
    
         return null;
    }
    

    所有这些都是这样使用的:

    [JsonConverter(typeof(JsonConverterDefaultGuid))]
    [NotDefault(ValidationMessages.FieldInvalid)]
    public Guid SectionId { get; set; }
    

    【讨论】:

      猜你喜欢
      • 2011-03-09
      • 2013-06-07
      • 2020-03-10
      • 1970-01-01
      • 2017-07-28
      • 1970-01-01
      • 2020-01-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多