如何使用 IValidatableObject?

Posted

技术标签:

【中文标题】如何使用 IValidatableObject?【英文标题】:How do I use IValidatableObject? 【发布时间】:2011-03-24 22:52:52 【问题描述】:

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

【问题讨论】:

【参考方案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://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator?view=net-6.0

【讨论】:

【参考方案2】:

引用Jeff Handley's Blog Post on Validation Objects and Properties with Validator:

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

    验证属性级属性 如果任何验证器无效,则中止验证并返回 失败 验证对象级属性 如果任何验证器无效,则中止验证并返回 失败 如果在桌面框架和对象上实现 IValidatableObject,然后调用它的 验证方法并返回任何 失败

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

你也可以看看Validator类here的确切实现

【讨论】:

【参考方案3】:

我不喜欢 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
            

【讨论】:

【参考方案4】:

我喜欢cocogza's answer,除了调用 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;
    

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

【讨论】:

【参考方案5】:

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

【讨论】:

【参考方案6】:

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

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;
        


    

【讨论】:

我喜欢这种使用命名参数的风格,让代码更容易阅读。【参考方案7】:

只是补充几点:

因为Validate() 方法签名返回IEnumerable&lt;&gt;,所以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 条目,如下所示(如果您在custom model binder 中进行验证,这可能很有用):

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 方法中使用属性和反射是否值得?【参考方案8】:

首先,感谢@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() 方法处理条件验证。

【讨论】:

我想不出我会使用它的场景。你能举个例子说明你会在哪里使用它吗? 如果您的表中有跟踪列(例如创建它的用户)。它在数据库中是必需的,但您在上下文中进入 SaveChanges 以填充它(开发人员无需记住显式设置它)。当然,您会在保存之前进行验证。因此,您无需根据需要标记“创建者”列,而是针对所有其他列/属性进行验证。 这个解决方案的问题是,现在您需要依赖调用者来正确验证您的对象。 为了增强这个答案,可以使用反射来查找所有具有验证属性的属性,然后调用 TryValidateProperty。

以上是关于如何使用 IValidatableObject?的主要内容,如果未能解决你的问题,请参考以下文章

使用 IValidatableObject 检查是不是设置了至少一个参数

在 IValidatableObject 上设置 ValidationContext 的 Items 集合?

是否可以在 IValidatableObject 方法中以强类型的方式指定字段名称?

如果在子级上验证失败,则不会在父级上调用IValidatableObject.Validate

EF 代码优先:IValidatable 对象未验证

如何在 ASP.NET Core 中实现自定义模型验证?