У меня есть ASP NET Core 2.1 API с ActionFilter (автоматическая проверка ModelState подавляется), и когда возникает ошибка привязки - например, недопустимая строка для привязки к guid - Состояние модели содержит только ошибки привязки, но не других ошибок - атрибут обязателен или MaxLength и т. д. Это что-то ожидается? И еще более важный вопрос: есть ли способ получить все ошибки состояния модели за одну поездку?

Фильтр My Action (глобальный):

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; }
}

Метод действия в контроллере

[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."
    ]
}

И когда действительны инструкции ID раздела:

[
    {
        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)"
    ]
}
0
StakAtak 25 Сен 2019 в 12:27

2 ответа

Лучший ответ

когда есть ошибка привязки - например, недопустимая строка для привязки к guid - Состояние модели содержит только ошибки привязки, но не других ошибок - требуется атрибут или MaxLength и т. д. Это что-то ожидается?

Это не правда. Настоящая причина в том, что вы обрабатываете полезную нагрузку JSON . Полезные данные json должны быть сначала десериализованы в ICollection<SkillBindDto>, а затем валидатор может их проверить.

При обработке вашей полезной нагрузки с недопустимым свойством 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
    }
}

Easy Walkaround

В качестве более удобного обхода вы можете отправить полезную нагрузку формы вместо application/json .

Полезные данные формы кодируются как пары "ключ-значение". разделены знаком '&', между ключом и значением ставится '=', что выглядит как строка запроса. Когда вы раньше отправляли HTML <form />, вы фактически отправляете полезные данные формы.

Frist, удалите атрибут [FromBody] в своем действии:

[HttpPost()]
public async Task<ActionResult<IReadOnlyCollection<SkillDto>>> Create([FromBody]ICollection<SkillBindDto> skills, CancellationToken cancellationToken)
{
    ...
}

А затем отправьте полезные данные в формате application/x-www-form-urlencoded. Вы можете использовать new FormData(formElementId) <> построить такую форму полезной нагрузки. Я создаю вспомогательную функцию, которая отображает JSON в form-data:

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 объекту form-data:

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

И тогда вы получите ответ, который описывает все ошибки:

enter image description here

0
itminus 26 Сен 2019 в 07:23

Я опубликую код, который решает мою проблему. Он основан на ответе минус. Итак, наконец, я использовал пользовательский JsonConverter для Guids, который работает нормально.

Пользовательский 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; }
0
StakAtak 27 Сен 2019 в 07:47