Як використовувати 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. Коли validateAllPropertiesfalse [Required], перевіряються лише властивості з атрибутом. Це дозволяє IValidatableObject.Validate()методу обробляти умовні перевірки.


Я не можу придумати сценарій, де я би цим скористався. Чи можете ви надати мені приклад, де ви б це використали?
Стефан Василевич

Якщо у вашій таблиці є стовпці відстеження (наприклад, користувач, який її створив). Це потрібно в базі даних, але ви вступаєте в SaveChanges в контексті, щоб заповнити його (виключивши необхідність розробників пам'ятати, щоб встановити це явно). Ви, звичайно, підтвердили б перед збереженням. Таким чином, ви не позначаєте стовпець "Творець" як необхідний, але перевіряєте його щодо всіх інших стовпців / властивостей.
MetalPhoenix

Проблема цього рішення полягає в тому, що тепер ви залежаєте від того, хто телефонує, щоб ваш об'єкт був належним чином перевірений.
cocogza

Щоб покращити цю відповідь, можна використати відображення, щоб знайти всі властивості, які мають атрибути перевірки, а потім зателефонувати в TryValidateProperty.
Пол Черноч

78

Цитата з публікації щоденника Джеффа Хендлі про об'єкти перевірки та властивості за допомогою валідатора :

Під час перевірки об'єкта в Validator.ValidateObject застосовується такий процес:

  1. Перевірка атрибутів рівня властивості
  2. Якщо будь-які валідатори недійсні, скасовуйте перевірку, повертаючи провал (и)
  3. Перевірка атрибутів рівня об’єкта
  4. Якщо будь-які валідатори недійсні, скасовуйте перевірку, повертаючи провал (и)
  5. Якщо на рамці робочого столу і об’єкт реалізує IValidatableObject, тоді викличте його метод Validate і поверніть будь-які збої

Це вказує на те, що те, що ви намагаєтеся зробити, не вийде поза рамками, оскільки перевірка буде перервана на кроці №2. Ви можете спробувати створити атрибути, які успадковуються від вбудованих, і спеціально перевірити наявність активованого властивості (через інтерфейс), перш ніж проводити їх нормальну перевірку. Крім того, ви можете вкласти всю логіку для перевірки сутності в Validateметоді.


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?
Шальк

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
Мені подобається, що стиль використання названих параметрів полегшує читання коду.
дризін

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
            {
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.