如何有选择地验证一些数据注释属性?
How to selectively validate some data annotation attribute?
我的视图模型中有一些属性在保存时是可选的,但在提交时是必需的。总之,我们允许部分保存,但是提交整个表单,我们确实希望确保所有必填字段都有值。
目前我能想到的唯一方法是:
处理 ModelState 错误集合。
视图模型具有所有 [Required]
属性。如果请求是部分保存,则在进入控制器操作时 ModelState.IsValid
变为 false
。然后我 运行 遍历所有 ModelState
(这是一个 ICollection<KeyValuePair<string, ModelState>>
)错误并删除由 [Required]
属性引发的所有错误。
但是如果请求是提交整个表单,我不会干扰ModelState
和[Required]
属性生效
使用不同的视图模型进行部分保存和提交
这个更难看。一个视图模型将包含所有 [Required]
属性,供操作方法用于提交。但是对于部分保存,我 post 将表单数据发送到不同的操作,该操作使用没有所有 [Required]
属性的相同视图模型。
显然,我最终会得到很多重复的代码/视图模型。
理想的解决方案
我一直在考虑是否可以为那些必需的属性创建自定义数据注释属性 [SubmitRequired]
。并以某种方式使验证在部分保存时忽略它,但在提交时不忽略它。
仍然没有明确的线索。任何人都可以帮忙吗?谢谢。
我认为您的问题有更精确的解决方案。假设您要提交给一种方法,我的意思是说您正在为部分提交和完整提交调用相同的方法。那么你应该像下面这样:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult YourMethod(ModelName model)
{
if(partialSave) // Check here whether it's a partial or full submit
{
ModelState.Remove("PropertyName");
ModelState.Remove("PropertyName2");
ModelState.Remove("PropertyName3");
}
if (ModelState.IsValid)
{
}
}
这应该可以解决您的问题。如果您遇到任何问题,请告诉我。
编辑:
正如@SBirthare 评论的那样,在更新模型时添加或删除属性是不可行的,我发现下面的解决方案应该适用于 [Required]
属性。
ModelState.Where(x => x.Value.Errors.Count > 0).Select(d => d.Key).ToList().ForEach(g => ModelState.Remove(g));
以上代码将获取所有可能出错的键并将它们从模型状态中删除。您需要将此行放在 if 条件中以确保它以部分形式提交运行。我还检查了错误只会出现在 [Required]
属性上(不知何故模型活页夹给予这个属性高优先级,即使你把它放在 after/below 任何其他属性)。让您再也不用担心模型更新了。
这是我在项目中使用的一种方法。
创建一个包含业务逻辑的 ValidationService<T>
,该业务逻辑将检查您的模型是否处于要使用 IsValidForSubmission
方法提交的有效状态。
将 IsSubmitting
属性 添加到您在调用 IsValidForSubmission
方法之前检查的视图模型。
仅使用内置验证属性检查无效数据,即字段长度等。
在不同的命名空间中创建一些自定义属性,这些属性将在某些情况下进行验证,即 [RequiredIfSubmitting]
,然后在您的服务中使用反射来迭代每个 属性 上的属性并调用它们的 IsValid
手动方法(跳过任何不在您的命名空间内的方法)。
这将填充 return 一个 Dictionary<string, string>
可用于填充 ModelState
回到 UI:
var validationErrors = _validationService.IsValidForSubmission(model);
if (validationErrors.Count > 0)
{
foreach (var error in validationErrors)
{
ModelState.AddModelError(error.Key, error.Value);
}
}
我的做法是添加条件检查注解属性,学习自foolproof。
使 SaveMode
成为视图模型的一部分。
将属性标记为可为空,以便在 SaveMode
不是 Finalize
时其值是可选的。
但添加自定义注释属性[FinalizeRequired]
:
[FinalizeRequired]
public int? SomeProperty { get; set; }
[FinalizeRequiredCollection]
public List<Item> Items { get; set; }
这是属性的代码:
[AttributeUsage(AttributeTargets.Property)]
public abstract class FinalizeValidationAttribute : ValidationAttribute
{
public const string DependentProperty = "SaveMode";
protected abstract bool IsNotNull(object value);
protected static SaveModeEnum GetSaveMode(ValidationContext validationContext)
{
var saveModeProperty = validationContext.ObjectType.GetProperty(DependentProperty);
if (saveModeProperty == null) return SaveModeEnum.Save;
return (SaveModeEnum) saveModeProperty.GetValue(validationContext.ObjectInstance);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var saveMode = GetSaveMode(validationContext);
if (saveMode != SaveModeEnum.SaveFinalize) return ValidationResult.Success;
return (IsNotNull(value))
? ValidationResult.Success
: new ValidationResult(string.Format("{0} is required when finalizing", validationContext.DisplayName));
}
}
对于原始数据类型,检查 value!=null
:
[AttributeUsage(AttributeTargets.Property)]
public class FinalizeRequiredAttribute : FinalizeValidationAttribute
{
protected override bool IsNotNull(object value)
{
return value != null;
}
}
对于 IEnumerable
个集合,
[AttributeUsage(AttributeTargets.Property)]
public class FinalizeRequiredCollectionAttribute : FinalizeValidationAttribute
{
protected override bool IsNotNull(object value)
{
var enumerable = value as IEnumerable;
return (enumerable != null && enumerable.GetEnumerator().MoveNext());
}
}
这种方法通过从控制器中移除验证逻辑来最好地实现关注点分离。数据注释属性应该处理那种工作,哪个控制器只需要检查 !ModelState.IsValid
。这在我的应用程序中特别有用,因为如果 ModelState
每个控制器的检查不同,我将无法重构到基本控制器中。
我的视图模型中有一些属性在保存时是可选的,但在提交时是必需的。总之,我们允许部分保存,但是提交整个表单,我们确实希望确保所有必填字段都有值。
目前我能想到的唯一方法是:
处理 ModelState 错误集合。
视图模型具有所有 [Required]
属性。如果请求是部分保存,则在进入控制器操作时 ModelState.IsValid
变为 false
。然后我 运行 遍历所有 ModelState
(这是一个 ICollection<KeyValuePair<string, ModelState>>
)错误并删除由 [Required]
属性引发的所有错误。
但是如果请求是提交整个表单,我不会干扰ModelState
和[Required]
属性生效
使用不同的视图模型进行部分保存和提交
这个更难看。一个视图模型将包含所有 [Required]
属性,供操作方法用于提交。但是对于部分保存,我 post 将表单数据发送到不同的操作,该操作使用没有所有 [Required]
属性的相同视图模型。
显然,我最终会得到很多重复的代码/视图模型。
理想的解决方案
我一直在考虑是否可以为那些必需的属性创建自定义数据注释属性 [SubmitRequired]
。并以某种方式使验证在部分保存时忽略它,但在提交时不忽略它。
仍然没有明确的线索。任何人都可以帮忙吗?谢谢。
我认为您的问题有更精确的解决方案。假设您要提交给一种方法,我的意思是说您正在为部分提交和完整提交调用相同的方法。那么你应该像下面这样:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult YourMethod(ModelName model)
{
if(partialSave) // Check here whether it's a partial or full submit
{
ModelState.Remove("PropertyName");
ModelState.Remove("PropertyName2");
ModelState.Remove("PropertyName3");
}
if (ModelState.IsValid)
{
}
}
这应该可以解决您的问题。如果您遇到任何问题,请告诉我。
编辑:
正如@SBirthare 评论的那样,在更新模型时添加或删除属性是不可行的,我发现下面的解决方案应该适用于 [Required]
属性。
ModelState.Where(x => x.Value.Errors.Count > 0).Select(d => d.Key).ToList().ForEach(g => ModelState.Remove(g));
以上代码将获取所有可能出错的键并将它们从模型状态中删除。您需要将此行放在 if 条件中以确保它以部分形式提交运行。我还检查了错误只会出现在 [Required]
属性上(不知何故模型活页夹给予这个属性高优先级,即使你把它放在 after/below 任何其他属性)。让您再也不用担心模型更新了。
这是我在项目中使用的一种方法。
创建一个包含业务逻辑的 ValidationService<T>
,该业务逻辑将检查您的模型是否处于要使用 IsValidForSubmission
方法提交的有效状态。
将 IsSubmitting
属性 添加到您在调用 IsValidForSubmission
方法之前检查的视图模型。
仅使用内置验证属性检查无效数据,即字段长度等。
在不同的命名空间中创建一些自定义属性,这些属性将在某些情况下进行验证,即 [RequiredIfSubmitting]
,然后在您的服务中使用反射来迭代每个 属性 上的属性并调用它们的 IsValid
手动方法(跳过任何不在您的命名空间内的方法)。
这将填充 return 一个 Dictionary<string, string>
可用于填充 ModelState
回到 UI:
var validationErrors = _validationService.IsValidForSubmission(model);
if (validationErrors.Count > 0)
{
foreach (var error in validationErrors)
{
ModelState.AddModelError(error.Key, error.Value);
}
}
我的做法是添加条件检查注解属性,学习自foolproof。
使 SaveMode
成为视图模型的一部分。
将属性标记为可为空,以便在 SaveMode
不是 Finalize
时其值是可选的。
但添加自定义注释属性[FinalizeRequired]
:
[FinalizeRequired]
public int? SomeProperty { get; set; }
[FinalizeRequiredCollection]
public List<Item> Items { get; set; }
这是属性的代码:
[AttributeUsage(AttributeTargets.Property)]
public abstract class FinalizeValidationAttribute : ValidationAttribute
{
public const string DependentProperty = "SaveMode";
protected abstract bool IsNotNull(object value);
protected static SaveModeEnum GetSaveMode(ValidationContext validationContext)
{
var saveModeProperty = validationContext.ObjectType.GetProperty(DependentProperty);
if (saveModeProperty == null) return SaveModeEnum.Save;
return (SaveModeEnum) saveModeProperty.GetValue(validationContext.ObjectInstance);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var saveMode = GetSaveMode(validationContext);
if (saveMode != SaveModeEnum.SaveFinalize) return ValidationResult.Success;
return (IsNotNull(value))
? ValidationResult.Success
: new ValidationResult(string.Format("{0} is required when finalizing", validationContext.DisplayName));
}
}
对于原始数据类型,检查 value!=null
:
[AttributeUsage(AttributeTargets.Property)]
public class FinalizeRequiredAttribute : FinalizeValidationAttribute
{
protected override bool IsNotNull(object value)
{
return value != null;
}
}
对于 IEnumerable
个集合,
[AttributeUsage(AttributeTargets.Property)]
public class FinalizeRequiredCollectionAttribute : FinalizeValidationAttribute
{
protected override bool IsNotNull(object value)
{
var enumerable = value as IEnumerable;
return (enumerable != null && enumerable.GetEnumerator().MoveNext());
}
}
这种方法通过从控制器中移除验证逻辑来最好地实现关注点分离。数据注释属性应该处理那种工作,哪个控制器只需要检查 !ModelState.IsValid
。这在我的应用程序中特别有用,因为如果 ModelState
每个控制器的检查不同,我将无法重构到基本控制器中。