MVC ICollection ValidationState始终设置为Skipped

作为ASP.NET Core MVC 1.0项目的一部分,我有一个带有ICollection属性的ViewModel。 我需要validation此集合包含一个或多个项目。 我的自定义validation属性未执行。

在我的实例中,它从multipart/form-data表单中保存多个文件附件。

我在ViewModel中使用自定义validation属性修饰了该属性:

 [RequiredCollection] public ICollection Attachments { get; set; } 

下面是自定义属性类。 它只是检查集合不是null并且元素大于零:

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

最后,在控制器中,我确保POST请求中的ViewModel有效,这应该触发validation:

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

如果我在ModelState.IsValid调用中断,则Attachments属性的ModelState.Values的内容为:

Visual Studio本地窗口

  • 为什么我的断点在RequiredCollectionAttribute.IsValid()方法中没有被击中?
  • 为什么ValidationState被设置为Skipped for Attachments属性?

编辑1

MethodViewModel定义,按要求:

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

编辑2

下面是actionContext.ModelState的修剪值(按JSON导出),如下所示。 这是在进入全局操作filterOnActionExecuting()时遇到断点的状态:

 { "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输入字段。

 
...
...

如果发现IFormFileIFormFile的集合不为null,MVC似乎会抑制进一步的validation。

如果您查看FormFileModelBinder.cs代码, 可以在此处查看问题 。 如果绑定器能够从上面的if / elseif / else子句获得非null结果,则禁止validation。

在测试中,我使用如下代码创建了一个视图模型:

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

当我实际上传文件到这个例子时,它上面的永远错误的属性永远不会被调用。

由于这是来自MVC本身,我认为你最好的选择是实现IValidateableObject接口。

 public class YourViewModel : IValidatableObject { public ICollection Attachments { get;set; } public IEnumerable 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(this IEnumerable collection) => collection == null || !collection.GetEnumerator().MoveNext(); 

更新

这已作为错误提交 ,应在1.0.0 RTM中修复。

部分答案 (仅用于代码共享)

试试这个:

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

另外,尝试添加filter。

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

并编写filter本身:

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

只是为了我们检查filter内是否触发了断点。