如何使用IValidatableObject?


182

我了解这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.
             */ 
        }
    }
}

Answers:


170

首先,感谢@ 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来填充它(从而使开发人员不必记住明确地对其进行设置)。当然,您将在保存之前进行验证。因此,您无需将“ creator”列标记为必填项,而是针对所有其他列/属性进行验证。
MetalPhoenix

该解决方案的问题在于,现在您依靠调用者来正确验证对象。
cocogza

为了增强此答案,可以使用反射来查找具有验证属性的所有属性,然后调用TryValidateProperty。
Paul Chernoch

78

引用Jeff Handley的博客文章有关使用Validator进行验证的对象和属性

验证对象时,将在Validator.ValidateObject中应用以下过程:

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

这表明您尝试执行的操作无法立即使用,因为验证将在步骤2中终止。您可以尝试创建从内置属性继承的属性,并在执行常规验证之前专门检查(通过接口)是否存在已启用的属性。或者,您可以将用于验证实体的所有逻辑放入Validate方法中。

您还可以在这里查看Validator类的确切实现


36

仅补充几点:

因为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方法中使用属性和反射是否值得?
沙尔克2015年

4

我实现了一个通用用法抽象类进行验证

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

0

可接受答案的问题在于,它现在依赖于调用者来正确验证对象。我要么删除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

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

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

这样效果更好!它不会崩溃,并且会产生一个不错的错误消息。希望这对某人有帮助!


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
            {
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.