Подтвердить что ты не робот

MVC ICollection <IFormFile> ValidationState всегда имеет значение Пропущен

Как часть проекта ASP.NET Core MVC 1.0, у меня есть ViewModel с свойством ICollection<>. Мне нужно проверить, что эта коллекция содержит один или несколько элементов. Мой пользовательский атрибут проверки не выполняется.

В моем экземпляре он содержит несколько прикрепленных файлов из формы multipart/form-data.

Я украсил свойство в ViewModel с помощью специального атрибута проверки:

[RequiredCollection]
public ICollection<IFormFile> Attachments { get; set; }

Ниже приведен пользовательский класс атрибутов. Он просто проверяет, что коллекция не равна нулю и имеет больше нуля:

public class RequiredCollectionAttribute : ValidationAttribute
{
    protected const string DefaultErrorMessageFormatString = "You must provide at least one.";

    public RequiredCollectionAttribute() : base(DefaultErrorMessageFormatString) { }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var collection = (ICollection) value;

        return collection == null || collection.Count > 0
            ? ValidationResult.Success
            : new ValidationResult(ErrorMessageString);
    }
}

И, наконец, в контроллере я гарантирую, что ViewModel в запросе POST действителен, что должно вызвать проверку:

[HttpPost]
public async Task<IActionResult> Method(MethodViewModel viewModel)
{
    if (!ModelState.IsValid)
        return View(viewModel);
    ...
}

Если я нарушаю вызов ModelState.IsValid, содержимое ModelState.Values для свойства Attachments:

Окно локаций Visual Studio

Вопрос

  • Почему моя точка останова в методе RequiredCollectionAttribute.IsValid() никогда не попадает?
  • Почему ValidationState устанавливается в Skipped для свойства Attachments?

-

Изменить 1:

Определение MethodViewModel в соответствии с запросом:

public class MethodViewModel
{
    ...
    [Display(Name = "Attachments")]
    [RequiredCollection(ErrorMessage = "You must attached at least one file.")]
    public ICollection<IFormFile> Attachments { get; set; }
    ...
}

-

Изменить 2:

Ниже приведено обрезанное значение actionContext.ModelState (экспортировано в JSON) по запросу. Это состояние, когда точка останова попадает на запись в глобальный фильтр действий, OnActionExecuting():

{
    "Count": 19,
    "ErrorCount": 0,
    "HasReachedMaxErrors": false,
    "IsReadOnly": false,
    "IsValid": true,
    "Keys": 
    [
        "Attachments"
    ], 
    "MaxAllowedErrors": 200,
    "ValidationState": Valid,
    "Values": 
    [
        {
            "AttemptedValue": null,
            {
            }, 
            "RawValue": null,
            "ValidationState": Microsoft.AspNet.Mvc.ModelBinding.ModelValidationState.Skipped
        }
    ], 
    {
        [
            "Key": "Attachments",
            {
                "AttemptedValue": null,
                "RawValue": null,
                "ValidationState": Microsoft.AspNet.Mvc.ModelBinding.ModelValidationState.Skipped
            }, 
            "key": "Attachments",
            {
                "AttemptedValue": null,
                "RawValue": null,
                "ValidationState": Microsoft.AspNet.Mvc.ModelBinding.ModelValidationState.Skipped
            } 
        ]
    } 
}

-

Изменить 3:

Синтаксис вида бритвы для отображения поля ввода Attachments.

<form role="form" asp-controller="Controller" asp-action="Method" method="post" enctype="multipart/form-data">
    ...
    <div class="form-group">
        <label asp-for="Attachments" class="control-label col-xs-3 col-sm-2"></label>
        <div class="col-xs-9 col-sm-10">
            <input asp-for="Attachments" class="form-control" multiple required>
            <span asp-validation-for="Attachments" class="text-danger"></span>
        </div>
    </div>
    ...
</form>
4b9b3361

Ответ 1

Похоже, что MVC подавляет дальнейшую проверку, если найденный IFormFile или набор IFormFile не равен нулю.

Если вы посмотрите на код FormFileModelBinder.cs, вы можете увидеть проблему прямо здесь. Он подавляет проверку, если связующее имеет возможность получить ненулевой результат из предложения if/elseif/else выше.

В тесте я сделал модель с таким кодом:

[ThisAttriuteAlwaysReturnsAValidationError]
public IFormFile Attachment { get;set; }

Когда я на самом деле загружаю файл в этот пример, атрибут всегда ошибочной над ним никогда не вызывается.

Поскольку это происходит от самого MVC, я считаю, что для вас лучше всего реализовать интерфейс IValidateableObject.

public class YourViewModel : IValidatableObject
{
    public ICollection<IFormFile> Attachments { get;set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var numAttachments = Attachments?.Count() ?? 0;
        if (numAttachments == 0)
        {
            yield return new ValidationResult(
                "You must attached at least one file.",
                new string[] { nameof(Attachments) });
        }
    }
}

Этот метод все равно будет вызываться, поскольку он не связан с каким-либо индивидуальным свойством и, таким образом, не будет подавлен MVC, как ваш атрибут.

Если вам нужно сделать это более чем в одном месте, вы можете создать способ расширения, чтобы помочь.

public static bool IsNullOrEmpty<T>(this IEnumerable<T> collection) =>
        collection == null || !collection.GetEnumerator().MoveNext();

Update

Это подано как ошибка и должно быть исправлено в 1.0.0 RTM.

Ответ 2

Частичный ответ (только для совместного использования кода)

попробуйте следующее:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class RequiredCollectionAttribute : ValidationAttribute
{

    public override bool IsValid(object value)
    {
        ErrorMessage = "You must provide at least one.";
        var collection = value as ICollection;

        return collection != null || collection.Count > 0;
    }
}

также попробуйте добавить фильтр.

GlobalConfiguration.Configuration.Filters.Add(new RequestValidationFilter());

и напишите сам фильтр:

public class RequestValidationFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.ModelState.IsValid == false)
        {
            var errors = actionContext.ModelState
                                      .Values
                                      .SelectMany(m => m.Errors
                                                        .Select(e => e.ErrorMessage));

            actionContext.Response = actionContext.Request.CreateErrorResponse(
                HttpStatusCode.BadRequest, actionContext.ModelState);

            actionContext.Response.ReasonPhrase = string.Join("\n", errors);
        }
    }
}

просто для нас, чтобы проверить, срабатывает ли точка останова внутри фильтра.