Как я могу использовать 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.
             */ 
        }
    }
}
НРР
источник

Ответы:

168

Прежде всего, спасибо @ 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 в контексте, чтобы заполнить его (устраняя необходимость для разработчиков не забывать устанавливать его явно). Вы, конечно, подтвердите перед сохранением. Таким образом, вы не помечаете столбец «создатель» как необходимый, а проверяете по всем другим столбцам / свойствам.
MetalPhoenix
Проблема с этим решением состоит в том, что теперь вы зависите от вызывающей стороны для правильной проверки вашего объекта.
cocogza
Чтобы улучшить этот ответ, можно использовать отражение, чтобы найти все свойства, которые имеют атрибуты проверки, а затем вызвать TryValidateProperty.
Павел Чернох
78

Цитата из сообщения Джеффа Хэндли в блоге об объектах и ​​свойствах проверки с помощью Validator :

При проверке объекта в Validator.ValidateObject применяется следующий процесс:

  1. Проверьте атрибуты уровня свойства
  2. Если какие-либо валидаторы недействительны, прервите валидацию, возвращая ошибки
  3. Проверьте атрибуты уровня объекта
  4. Если какие-либо валидаторы недействительны, прервите валидацию, возвращая ошибки
  5. Если на платформе рабочего стола и объект реализует IValidatableObject, то вызовите его метод Validate и верните любые ошибки

Это означает, что то, что вы пытаетесь сделать, не будет работать «из коробки», поскольку проверка будет прервана на шаге № 2. Вы можете попытаться создать атрибуты, которые наследуются от встроенных, и, в частности, проверить наличие включенного свойства (через интерфейс) перед выполнением их обычной проверки. В качестве альтернативы, вы можете поместить всю логику для проверки сущности в Validateметоде.

Крис Кричит
источник
36

Просто добавлю пару пунктов:

Поскольку Validate()возвращается сигнатура метода IEnumerable<>, это yield returnможет использоваться для ленивой генерации результатов - это полезно, если некоторые из проверочных проверок требуют интенсивного ввода-вывода или загрузки ЦП.

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)));
}
StuartLC
источник
Хороший! Стоит ли использовать атрибуты и отражения в методе Validate?
Шальк
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;
        }


    }
}
guneysus
источник
1
Мне нравится этот стиль использования именованных параметров, который делает код намного проще для чтения.
Дризин
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;
    }
}
cocogza
источник
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;
    }
}

Это работает намного лучше! Он не падает и выдает хорошее сообщение об ошибке. Надеюсь, это поможет кому-то!

rjacobsen0
источник
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
            {
Джон Лорд
источник