如何使用 IValidatableObject?

发布于 2024-09-12 08:10:57 字数 869 浏览 4 评论 0原文

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

I understand that IValidatableObject is used to validate an object in a way that lets one compare properties against each other.

I'd still like to have attributes to validate individual properties, but I want to ignore failures on some properties in certain cases.

Am I trying to use it incorrectly in the case below? If not how do I implement this?

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.
             */ 
        }
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

破晓 2024-09-19 08:10:57

首先,感谢@paper1337为我指出了正确的资源......我没有注册,所以我不能投票给他,如果其他人读到了这篇文章,请这样做。

以下是如何完成我想要做的事情。

Validatable 类:

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() 方法处理条件验证。

First off, thanks to @paper1337 for pointing me to the right resources...I'm not registered so I can't vote him up, please do so if anybody else reads this.

Here's how to accomplish what I was trying to do.

Validatable class:

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

Using Validator.TryValidateProperty() will add to the results collection if there are failed validations. If there is not a failed validation then nothing will be add to the result collection which is an indication of success.

Doing the validation:

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

It is important to set validateAllProperties to false for this method to work. When validateAllProperties is false only properties with a [Required] attribute are checked. This allows the IValidatableObject.Validate() method handle the conditional validations.

哭泣的笑容 2024-09-19 08:10:57

引用 Jeff Handley 关于使用验证器验证对象和属性的博客文章

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

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

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

您还可以查看 Validator此处

Quote from Jeff Handley's Blog Post on Validation Objects and Properties with Validator:

When validating an object, the
following process is applied in
Validator.ValidateObject:

  1. Validate property-level attributes
  2. If any validators are invalid, abort validation returning the
    failure(s)
  3. Validate the object-level attributes
  4. If any validators are invalid, abort validation returning the
    failure(s)
  5. If on the desktop framework and the object implements
    IValidatableObject, then call its
    Validate method and return any
    failure(s)

This indicates that what you are attempting to do won't work out-of-the-box because the validation will abort at step #2. You could try to create attributes that inherit from the built-in ones and specifically check for the presence of an enabled property (via an interface) before performing their normal validation. Alternatively, you could put all of the logic for validating the entity in the Validate method.

You also could take a look a the exact implemenation of Validator class here

∝单色的世界 2024-09-19 08:10:57

只需添加几点:

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

Just to add a couple of points:

Because the Validate() method signature returns IEnumerable<>, that yield return can be used to lazily generate the results - this is beneficial if some of the validation checks are IO or CPU intensive.

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

Also, if you are using MVC ModelState, you can convert the validation result failures to ModelState entries as follows (this might be useful if you are doing the validation in a 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)));
}
别忘他 2024-09-19 08:10:57

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

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


    }
}

I implemented a general usage abstract class for validation

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


    }
}
同展鸳鸯锦 2024-09-19 08:10:57

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

The problem with the accepted answer is that it now depends on the caller for the object to be properly validated. I would either remove the RangeAttribute and do the range validation inside the Validate method or I would create a custom attribute subclassing RangeAttribute that takes the name of the required property as an argument on the constructor.

For example:

[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;
    }
}
梦醒灬来后我 2024-09-19 08:10:57

使用 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

Implement validation logic using IValidatableObject or property level validation (attributes) than use System.ComponentModel.DataAnnotations.Validator class like this

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

any errors should be present in validations collection (ErrorMessage property should be not empty).

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

愿与i 2024-09-19 08:10:57

你可以让它变得更简单。

假设我们有两个属性:经度和纬度。它们都是必需的,但仅当其中之一被提交时才需要。(如果我们填写经度,我们还需要提供纬度,反之亦然)

从类中的 IValidatableObject 继承并实现 >validate 方法

public class MyLocationModel: IValidatableObject

    {
    
public decimal? Longitude { get; set; }

public decimal? Latitude { get; set; }
    
    
             public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
                    if (Latitude.HasValue && !Longitude.HasValue)
                        yield return new ValidationResult(errorMessage: "Longitude is required if latitude is filled in.", new[] { nameof(Longitude) });
        
                    if (Longitude.HasValue && !Latitude.HasValue)
                        yield return new ValidationResult(errorMessage: "Latitude is required if longitude is filled in.", new[] { nameof(Latitude) });
        }
        }

如果您验证 MyLocationModel 的实例,规则将自动应用:)

完整说明 此处

You can make it so much simpler.

Say we have two properties: longitude and latitude. They are both required but only if one of them is filed in. (If we fill in longitude we need to provide a latitude as well and visa versa)

Inherit from IValidatableObject in your class and implement the validate method

public class MyLocationModel: IValidatableObject

    {
    
public decimal? Longitude { get; set; }

public decimal? Latitude { get; set; }
    
    
             public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
                    if (Latitude.HasValue && !Longitude.HasValue)
                        yield return new ValidationResult(errorMessage: "Longitude is required if latitude is filled in.", new[] { nameof(Longitude) });
        
                    if (Longitude.HasValue && !Latitude.HasValue)
                        yield return new ValidationResult(errorMessage: "Latitude is required if longitude is filled in.", new[] { nameof(Latitude) });
        }
        }

The rules will automatically apply if you validate an instance of the MyLocationModel :)

Full explanation here.

送舟行 2024-09-19 08:10:57

我喜欢 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;
    }
}

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

I liked cocogza's answer except that calling base.IsValid resulted in a stack overflow exception as it would re-enter the IsValid method again and again. So I modified it to be for a specific type of validation, in my case it was for an e-mail address.

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

This works much better! It doesn't crash and produces a nice error message. Hope this helps someone!

倚栏听风 2024-09-19 08:10:57

iValidate 有几个弱点。第一个是在所有其他验证之后运行,因此在修复模型验证错误之前您甚至不会看到结果。
此外,由于它是模型的一部分,因此每次将数据复制到新模型时以及仅显示尚未编辑的数据时,它都会针对数据集中的每一行运行。
您可以创建自定义注释来执行与 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
            {

iValidate has a couple of weaknesses. The first is that is runs AFTER all other validation, so you won't even see the results until you fix model validation errors.
Additionally, since it is part of the model, it runs every time you copy data into a new model, and for every row in the dataset when merely displaying data you haven't edited.
You could create custom annotations to do the same thing as iValidate and they would add the results to modelstate. However, I would suggest you simply create a function and place all your save validation code in that. Alternately, for websites, you could have your "special" validation in the controller after the model is created. Example:

 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
            {
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文