¿Cómo uso IValidatableObject?

182

Entiendo que IValidatableObjectse usa para validar un objeto de una manera que le permite a uno comparar propiedades entre sí.

Todavía me gustaría tener atributos para validar propiedades individuales, pero quiero ignorar fallas en algunas propiedades en ciertos casos.

¿Estoy tratando de usarlo incorrectamente en el siguiente caso? Si no, ¿cómo implemento esto?

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
fuente

Respuestas:

168

En primer lugar, gracias a @ paper1337 por señalarme los recursos correctos ... No estoy registrado, así que no puedo votarlo, por favor hágalo si alguien más lee esto.

Aquí se explica cómo lograr lo que estaba tratando de hacer.

Clase validable:

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

El uso Validator.TryValidateProperty()se agregará a la colección de resultados si hay validaciones fallidas. Si no hay una validación fallida, no se agregará nada a la colección de resultados, lo cual es una indicación de éxito.

Haciendo la validación:

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

Es importante establecerlo validateAllPropertiesen falso para que este método funcione. Cuando validateAllPropertieses falso solo [Required]se verifican las propiedades con un atributo. Esto permite que el IValidatableObject.Validate()método maneje las validaciones condicionales.

zrg
fuente
No puedo pensar en un escenario en el que usaría esto. ¿Me puede dar un ejemplo de dónde usaría esto?
Stefan Vasiljevic
Si tiene columnas de seguimiento en su tabla (como el usuario que la creó). Se requiere en la base de datos, pero usted ingresa en SaveChanges en el contexto para completarla (eliminando la necesidad de que los desarrolladores recuerden configurarla explícitamente). Por supuesto, debería validar antes de guardar. Por lo tanto, no marca la columna "creador" como se requiere, sino que la valida contra todas las demás columnas / propiedades.
MetalPhoenix
El problema con esta solución es que ahora depende de la persona que llama para que su objeto se valide correctamente.
cocogza
Para mejorar esta respuesta, uno podría usar la reflexión para encontrar todas las propiedades que tienen atributos de validación, luego llamar a TryValidateProperty.
Paul Chernoch
78

Cita de la publicación del blog de Jeff Handley sobre objetos de validación y propiedades con el validador :

Al validar un objeto, el siguiente proceso se aplica en Validator.ValidateObject:

  1. Validar atributos de nivel de propiedad
  2. Si alguno de los validadores no es válido, cancele la validación y devuelva los fallos.
  3. Validar los atributos a nivel de objeto
  4. Si alguno de los validadores no es válido, cancele la validación y devuelva los fallos.
  5. Si en el marco de escritorio y el objeto implementa IValidatableObject, llame a su método Validate y devuelva cualquier falla (s)

Esto indica que lo que está intentando hacer no funcionará de inmediato porque la validación se cancelará en el paso 2. Podría intentar crear atributos que hereden de los integrados y verificar específicamente la presencia de una propiedad habilitada (a través de una interfaz) antes de realizar su validación normal. Alternativamente, podría poner toda la lógica para validar la entidad en el Validatemétodo.

Chris grita
fuente
36

Solo para agregar un par de puntos:

Debido a que la Validate()firma del método regresa IEnumerable<>, eso yield returnpuede usarse para generar perezosamente los resultados; esto es beneficioso si algunas de las comprobaciones de validación son intensivas en IO o 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");
        }

Además, si está utilizando MVC ModelState, puede convertir las fallas del resultado de la validación en ModelStateentradas de la siguiente manera (esto podría ser útil si está realizando la validación en un cuaderno de modelo personalizado ):

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
fuente
¡Buena esa! ¿Merece la pena usar atributos y reflexión en el método Validar?
Schalk
4

Implementé una clase abstracta de uso general para validación

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
fuente
1
Me encanta ese estilo de usar parámetros con nombre, hace que el código sea mucho más fácil de leer.
drizin
0

El problema con la respuesta aceptada es que ahora depende del llamador para que el objeto se valide correctamente. Quitaría RangeAttribute y haría la validación de rango dentro del método Validate o crearía un atributo personalizado que subclase RangeAttribute que tome el nombre de la propiedad requerida como argumento en el constructor.

Por ejemplo:

[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
fuente
0

Me gustó la respuesta de cocogza, excepto esa base de llamadas. IsValid resultó en una excepción de desbordamiento de pila, ya que volvería a ingresar al método IsValid una y otra vez. Así que lo modifiqué para un tipo específico de validación, en mi caso fue para una dirección de correo electrónico.

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

¡Esto funciona mucho mejor! No se bloquea y produce un buen mensaje de error. ¡Espero que esto ayude a alguien!

rjacobsen0
fuente
0

Lo que no me gusta de iValidate es que parece ejecutarse DESPUÉS de todas las demás validaciones.
Además, al menos en nuestro sitio, volvería a ejecutarse durante un intento de guardar. Le sugiero que simplemente cree una función y coloque todo su código de validación en eso. Alternativamente para sitios web, puede tener su validación "especial" en el controlador después de crear el modelo. Ejemplo:

 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
fuente