How do I use IValidatableObject?

2018-12-31 07:28发布

I understand that IValidatableObject is used to validate an object in a way that let's 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.
         */ 
    }
}
}

6条回答
萌妹纸的霸气范
2楼-- · 2018-12-31 07:50

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.

查看更多
回忆,回不去的记忆
3楼-- · 2018-12-31 07:53

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.

查看更多
忆尘夕之涩
4楼-- · 2018-12-31 07:56

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)));
}
查看更多
皆成旧梦
5楼-- · 2018-12-31 07:56

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!

查看更多
素衣白纱
6楼-- · 2018-12-31 07:57

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


    }
}
查看更多
与君花间醉酒
7楼-- · 2018-12-31 08:05

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;
    }
}
查看更多
登录 后发表回答