Jak używać IValidatableObject?

182

Rozumiem, że IValidatableObjectjest to używane do walidacji obiektu w sposób, który pozwala porównać właściwości ze sobą.

Nadal chciałbym mieć atrybuty do walidacji poszczególnych właściwości, ale w niektórych przypadkach chcę ignorować awarie niektórych właściwości.

Czy próbuję go użyć nieprawidłowo w poniższym przypadku? Jeśli nie, jak mam to zaimplementować?

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.
             */ 
        }
    }
}
zrg
źródło

Odpowiedzi:

168

Po pierwsze, dzięki @ paper1337 za wskazanie mi odpowiednich zasobów ... Nie jestem zarejestrowany, więc nie mogę na niego głosować, zrób to, jeśli ktoś inny to przeczyta.

Oto, jak osiągnąć to, co próbowałem zrobić.

Walidowalna klasa:

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

Użycie Validator.TryValidateProperty()spowoduje dodanie do kolekcji wyników w przypadku niepowodzenia weryfikacji. Jeśli nie ma niepowodzenia walidacji, nic nie zostanie dodane do kolekcji wyników, co wskazuje na sukces.

Przeprowadzanie walidacji:

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

validateAllPropertiesAby ta metoda działała, należy ustawić wartość false. Gdy validateAllPropertiesjest fałszywe, [Required]sprawdzane są tylko właściwości z atrybutem. Pozwala to IValidatableObject.Validate()metodzie obsłużyć walidacje warunkowe.

zrg
źródło
Nie mogę wymyślić scenariusza, w którym bym tego użył. Czy możesz podać przykład, gdzie możesz tego użyć?
Stefan Vasiljevic,
Jeśli masz w tabeli kolumny śledzenia (na przykład użytkownika, który ją utworzył). Jest to wymagane w bazie danych, ale wkraczasz w SaveChanges w kontekście, aby go wypełnić (eliminując potrzebę, aby programiści pamiętali o jawnym ustawieniu). Oczywiście przed zapisaniem zweryfikowałbyś. Dlatego nie oznaczasz kolumny „twórca” jako wymaganej, ale przeprowadzasz weryfikację pod kątem wszystkich innych kolumn / właściwości.
MetalPhoenix
Problem z tym rozwiązaniem polega na tym, że teraz jesteś zależny od obiektu wywołującego, aby obiekt został poprawnie zweryfikowany.
cocogza
Aby ulepszyć tę odpowiedź, można użyć odbicia, aby znaleźć wszystkie właściwości, które mają atrybuty walidacji, a następnie wywołać TryValidateProperty.
Paul Chernoch,
78

Cytat z posta na blogu Jeffa Handleya na temat obiektów walidacyjnych i właściwości za pomocą walidatora :

Podczas walidacji obiektu w Validator.ValidateObject stosowany jest następujący proces:

  1. Sprawdź poprawność atrybutów na poziomie właściwości
  2. Jeśli któryś z walidatorów jest nieprawidłowy, przerwij walidację, zwracając błędy
  3. Sprawdź poprawność atrybutów na poziomie obiektu
  4. Jeśli któryś z walidatorów jest nieprawidłowy, przerwij walidację, zwracając błędy
  5. Jeśli w środowisku pulpitu i obiekcie zaimplementowano IValidatableObject, wywołaj jego metodę Validate i zwróć wszelkie niepowodzenia

Oznacza to, że to, co próbujesz zrobić, nie zadziała po wyjęciu z pudełka, ponieważ walidacja zostanie przerwana w kroku 2. Możesz spróbować utworzyć atrybuty, które dziedziczą po wbudowanych, a konkretnie sprawdzić obecność włączonej właściwości (za pośrednictwem interfejsu) przed przeprowadzeniem ich normalnej weryfikacji. Alternatywnie, możesz umieścić całą logikę walidacji jednostki w Validatemetodzie.

Chris krzyczy
źródło
36

Wystarczy dodać kilka punktów:

Ponieważ Validate()sygnatura metody powraca IEnumerable<>, yield returnmożna jej użyć do leniwego generowania wyników - jest to korzystne, jeśli niektóre kontrole walidacji wymagają intensywnych operacji we / wy lub procesora.

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

Ponadto, jeśli używasz MVC ModelState, możesz przekonwertować niepowodzenia wyniku weryfikacji na ModelStatewpisy w następujący sposób (może to być przydatne, jeśli przeprowadzasz walidację w segregatorze modelu niestandardowego ):

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
źródło
Niezłe! Czy warto używać atrybutów i refleksji w metodzie Validate?
Schalk
4

Zaimplementowałem klasę abstrakcyjną ogólnego zastosowania do walidacji

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
źródło
1
Uwielbiam ten styl używania nazwanych parametrów, dzięki czemu kod jest znacznie łatwiejszy do odczytania.
drizin
0

Problem z zaakceptowaną odpowiedzią polega na tym, że teraz od wywołującego zależy, czy obiekt zostanie poprawnie zweryfikowany. Chciałbym albo usunąć RangeAttribute i wykonać walidację zakresu w ramach metody Validate, albo utworzyć podklasę atrybutu niestandardowego RangeAttribute, która przyjmuje nazwę wymaganej właściwości jako argument w konstruktorze.

Na przykład:

[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
źródło
0

Podobała mi się odpowiedź cocogzy, z wyjątkiem tego, że wywołanie base.IsValid spowodowało wyjątek przepełnienia stosu, ponieważ wielokrotnie wprowadzał metodę IsValid. Więc zmodyfikowałem go tak, aby był dla określonego typu walidacji, w moim przypadku był to adres e-mail.

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

To działa znacznie lepiej! Nie ulega awarii i wyświetla ładny komunikat o błędzie. Mam nadzieję, że to komuś pomoże!

rjacobsen0
źródło
0

To, czego nie lubię w iValidate, to wygląda na to, że działa tylko PO wszystkich innych walidacjach.
Dodatkowo, przynajmniej w naszej witrynie, uruchomiłoby się ponownie podczas próby zapisu. Sugerowałbym po prostu utworzenie funkcji i umieszczenie w niej całego kodu walidacyjnego. Alternatywnie w przypadku witryn internetowych możesz mieć swoją „specjalną” walidację w kontrolerze po utworzeniu modelu. Przykład:

 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
            {
John Lord
źródło