我该如何使用IValidatableObject?

207

我知道IValidatableObject是用于验证对象的一种方式,可以让我们将属性相互比较。

我仍然希望有属性来验证单个属性,但我想在某些情况下忽略某些属性的失败。

如果我在下面的情况下没有使用不正确,请问我该如何实现?

public class ValidateMe : IValidatableObject
{
    [Required]
    public bool Enable { get; set; }

    [Range(1, 5)]
    public int Prop1 { get; set; }

    [Range(1, 5)]
    public int Prop2 { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!this.Enable)
        {
            /* Return valid result here.
             * I don't care if Prop1 and Prop2 are out of range
             * if the whole object is not "enabled"
             */
        }
        else
        {
            /* Check if Prop1 and Prop2 meet their range requirements here
             * and return accordingly.
             */ 
        }
    }
}
8个回答

203

首先,感谢 @paper1337 指引我找到正确的资源...我没有注册所以无法为他点赞,如果还有其他人看到这个,请给他点赞。

以下是如何完成我所尝试的任务。

可验证类:

public class ValidateMe : IValidatableObject
{
    [Required]
    public bool Enable { get; set; }

    [Range(1, 5)]
    public int Prop1 { get; set; }

    [Range(1, 5)]
    public int Prop2 { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var results = new List<ValidationResult>();
        if (this.Enable)
        {
            Validator.TryValidateProperty(this.Prop1,
                new ValidationContext(this, null, null) { MemberName = "Prop1" },
                results);
            Validator.TryValidateProperty(this.Prop2,
                new ValidationContext(this, null, null) { MemberName = "Prop2" },
                results);

            // some other random test
            if (this.Prop1 > this.Prop2)
            {
                results.Add(new ValidationResult("Prop1 must be larger than Prop2"));
            }
        }
        return results;
    }
}

使用 Validator.TryValidateProperty() 方法,如果存在验证失败,则会添加到结果集合中。 如果没有失败的验证,则不会向结果集合中添加任何内容,这表明验证成功。

进行验证:

    public void DoValidation()
    {
        var toValidate = new ValidateMe()
        {
            Enable = true,
            Prop1 = 1,
            Prop2 = 2
        };

        bool validateAllProperties = false;

        var results = new List<ValidationResult>();

        bool isValid = Validator.TryValidateObject(
            toValidate,
            new ValidationContext(toValidate, null, null),
            results,
            validateAllProperties);
    }

为了使这个方法正常工作,将 validateAllProperties 设置为 false 是很重要的。当 validateAllProperties 为 false 时,只有带有 [Required] 属性的属性才会被检查。这样可以让 IValidatableObject.Validate() 方法处理条件验证。


我想不出我会在哪种情况下使用这个。你能给我一个你会使用它的例子吗? - Stefan Vasiljevic
如果您的表中有跟踪列(例如创建它的用户),则在数据库中需要该列,但是您可以在上下文中的SaveChanges中填充它(消除了开发人员明确设置它的需求)。当然,在保存之前需要进行验证。因此,您不会将“创建者”列标记为必需,而是针对所有其他列/属性进行验证。 - user4593252
这个解决方案的问题在于,现在你依赖调用者来正确验证你的对象。 - cocogza
2
为了增强这个答案,可以使用反射来查找所有具有验证属性的属性,然后调用TryValidateProperty。 - Paul Chernoch

87

以下是来自Jeff Handley关于使用Validator进行验证对象和属性的博客文章的摘录:

当验证一个对象时,Validator.ValidateObject会按照以下过程进行:

  1. 验证属性级别的属性
  2. 如果任何验证器无效,则中止验证并返回失败信息
  3. 验证对象级别的属性
  4. 如果任何验证器无效,则中止验证并返回失败信息
  5. 如果在桌面框架上,且该对象实现了IValidatableObject,则调用其Validate方法并返回任何失败信息

这表明你尝试做的事情不会立即成功,因为验证将在步骤#2中停止。你可以尝试创建从内置验证器继承并在执行正常验证之前专门检查启用属性(通过接口)是否存在的属性。或者,你可以将验证实体的所有逻辑放入Validate方法中。

你也可以查看Validator类的确切实现这里


42

仅仅添加几点:

由于 Validate() 方法的签名返回 IEnumerable<>,因此可以使用 yield return 来惰性生成结果——如果某些验证检查是 IO 或 CPU 密集型的,这将非常有益。

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
    if (this.Enable)
    {
        // ...
        if (this.Prop1 > this.Prop2)
        {
            yield return new ValidationResult("Prop1 must be larger than Prop2");
        }

另外,如果您正在使用 MVC ModelState,则可以按以下方式将验证结果失败转换为 ModelState 条目(如果您在自定义模型绑定程序中进行验证,这可能很有用):

var resultsGroupedByMembers = validationResults
    .SelectMany(vr => vr.MemberNames
                        .Select(mn => new { MemberName = mn ?? "", 
                                            Error = vr.ErrorMessage }))
    .GroupBy(x => x.MemberName);

foreach (var member in resultsGroupedByMembers)
{
    ModelState.AddModelError(
        member.Key,
        string.Join(". ", member.Select(m => m.Error)));
}

不错!在Validate方法中使用属性和反射值得吗? - Schalk

5

我为验证实现了一个通用使用的抽象类

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace App.Abstractions
{
    [Serializable]
    abstract public class AEntity
    {
        public int Id { get; set; }

        public IEnumerable<ValidationResult> Validate()
        {
            var vResults = new List<ValidationResult>();

            var vc = new ValidationContext(
                instance: this,
                serviceProvider: null,
                items: null);

            var isValid = Validator.TryValidateObject(
                instance: vc.ObjectInstance,
                validationContext: vc,
                validationResults: vResults,
                validateAllProperties: true);

            /*
            if (true)
            {
                yield return new ValidationResult("Custom Validation","A Property Name string (optional)");
            }
            */

            if (!isValid)
            {
                foreach (var validationResult in vResults)
                {
                    yield return validationResult;
                }
            }

            yield break;
        }


    }
}

1
我喜欢使用命名参数的风格,这样可以使代码更易于阅读。 - drizin

1

使用IValidatableObject或属性级别验证(属性)实现验证逻辑,然后像这样使用System.ComponentModel.DataAnnotations.Validator类:

var validationContext = new ValidationContext(model,, null, null);
var validations = new Collection<ValidationResult>();
Validator.TryValidaObject(model, validationContext, validations, true)

任何错误都应该出现在验证集合中(ErrorMessage 属性不应为空)。

https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator?view=net-6.0


1
接受答案的问题在于现在取决于调用者来正确验证对象。我要么删除RangeAttribute并在Validate方法中执行范围验证,要么创建一个自定义属性,继承RangeAttribute并在构造函数中使用所需属性的名称作为参数。
例如:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
class RangeIfTrueAttribute : RangeAttribute
{
    private readonly string _NameOfBoolProp;

    public RangeIfTrueAttribute(string nameOfBoolProp, int min, int max) : base(min, max)
    {
        _NameOfBoolProp = nameOfBoolProp;
    }

    public RangeIfTrueAttribute(string nameOfBoolProp, double min, double max) : base(min, max)
    {
        _NameOfBoolProp = nameOfBoolProp;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var property = validationContext.ObjectType.GetProperty(_NameOfBoolProp);
        if (property == null)
            return new ValidationResult($"{_NameOfBoolProp} not found");

        var boolVal = property.GetValue(validationContext.ObjectInstance, null);

        if (boolVal == null || boolVal.GetType() != typeof(bool))
            return new ValidationResult($"{_NameOfBoolProp} not boolean");

        if ((bool)boolVal)
        {
            return base.IsValid(value, validationContext);
        }
        return null;
    }
}

0
我不喜欢iValidate的一件事是它似乎只在所有其他验证之后运行。
此外,在我们的网站上,它会在保存尝试期间再次运行。我建议您只需创建一个函数并将所有验证代码放入其中。或者对于网站,您可以在模型创建后在控制器中进行“特殊”验证。例如:

 public ActionResult Update([DataSourceRequest] DataSourceRequest request, [Bind(Exclude = "Terminal")] Driver driver)
    {

        if (db.Drivers.Where(m => m.IDNumber == driver.IDNumber && m.ID != driver.ID).Any())
        {
            ModelState.AddModelError("Update", string.Format("ID # '{0}' is already in use", driver.IDNumber));
        }
        if (db.Drivers.Where(d => d.CarrierID == driver.CarrierID
                                && d.FirstName.Equals(driver.FirstName, StringComparison.CurrentCultureIgnoreCase)
                                && d.LastName.Equals(driver.LastName, StringComparison.CurrentCultureIgnoreCase)
                                && (driver.ID == 0 || d.ID != driver.ID)).Any())
        {
            ModelState.AddModelError("Update", "Driver already exists for this carrier");
        }

        if (ModelState.IsValid)
        {
            try
            {

0

除了调用 base.IsValid 导致堆栈溢出异常,因为它会一遍又一遍地重新进入 IsValid 方法之外,我喜欢 cocogza 的答案。所以我将其修改为针对特定类型的验证,就我而言,这是针对电子邮件地址的。

[AttributeUsage(AttributeTargets.Property)]
class ValidEmailAddressIfTrueAttribute : ValidationAttribute
{
    private readonly string _nameOfBoolProp;

    public ValidEmailAddressIfTrueAttribute(string nameOfBoolProp)
    {
        _nameOfBoolProp = nameOfBoolProp;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (validationContext == null)
        {
            return null;
        }

        var property = validationContext.ObjectType.GetProperty(_nameOfBoolProp);
        if (property == null)
        {
            return new ValidationResult($"{_nameOfBoolProp} not found");
        }

        var boolVal = property.GetValue(validationContext.ObjectInstance, null);

        if (boolVal == null || boolVal.GetType() != typeof(bool))
        {
            return new ValidationResult($"{_nameOfBoolProp} not boolean");
        }

        if ((bool)boolVal)
        {
            var attribute = new EmailAddressAttribute {ErrorMessage = $"{value} is not a valid e-mail address."};
            return attribute.GetValidationResult(value, validationContext);
        }
        return null;
    }
}

这个方法好多了!它不会崩溃,并且会产生一个漂亮的错误信息。希望能对某些人有所帮助!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接