jueves, 19 de marzo de 2015

Distintas opciones para validar el modelo en ASP.NET MVC

La validación del modelo en ASP.NET MVC es un característica muy importante ya que garantiza que la lógica de la entidad que recibe el controlador es conforme a la reglas de validación que hayamos definido para la entidad.

Hace tiempo publiqué un post relacionado con la validación del modelo pero estaba más centrado en cómo implementar la validación en el lado del cliente Validación en cliente en ASP.NET MVC. Sin embargo, repasando ahora las distintas opciones que tenemos disponibles para validar el modelo en ASP.NET MVC, reconozco que al ser varias no siempre es sencillo decidir cual de ellas es más apropiada según qué escenario.

Que yo sepa hay hasta 4 distintas formas de implementar la validación del modelo:

La validación del modelo es lanzada por los proveedores de validación del modelo, que inicialmente es una colección con 3 proveedores:

private void DebugModelValidatorProviders()
{
foreach (var provider in ModelValidatorProviders.Providers)
{
System.Diagnostics.Debug.WriteLine(provider.GetType().Name);
}
//DataAnnotationsModelValidatorProvider
//DataErrorInfoModelValidatorProvider
//ClientDataTypeModelValidatorProvider
}

El proveedor DataAnnotationsModelValidatorProvider es quien se encargará de llamar a CustomValidationAttribute, ValidationAttribute e IValidatableObject. Por otro lado, DataErrorInfoModelValidatorProvider llamará a IDataErrorInfo.

Sabiendo ya que opciones tenemos ¿Cuál utilizar?

Reconozco que en mi caso he usado siempre ValidationAttribute, pero intentaré en este post sopesar pros y contras de todas las opciones disponibles (así después no tendré que volver a hacer el ejercicio de reflexión si más adelante lo necesito).

Para todos los ejemplos utilizaremos un ViewModel muy sencillo:

public class PersonViewModel
{
[Required]
[Display(Name = "Nombre")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Edad")]
public int Age { get; set; }
}


Un controlador
using System.Web.Mvc;
using MyValidation.ViewModels;

namespace MyValidation.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new PersonViewModel();
return View(model);
}

[HttpPost]
public ActionResult Index(PersonViewModel model)
{
return View(model);
}
}
}

Y una vista:
@model MyValidation.ViewModels.PersonViewModel
<div class="container">
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(m => m.FirstName, new { @class = "control-label" })
@Html.TextBoxFor(m => m.FirstName, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.FirstName)
</div>
<div class="form-group">
@Html.LabelFor(m => m.Age, new { @class = "control-label" })
@Html.TextBoxFor(m => m.Age, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Age)
</div>
<button type="submit" class="btn btn-default">Submit</button>
}
</div>


CustomValidationAttribute

La idea es crear un método estático por cada propiedad que se quiera validar. Si además queremos validar la entidad como conjunto, crearemos igualmente otro método estático. Después decoraremos la entidad y/o cada propiedad con el atributo CustomValidation donde especificaremos que método tiene que llamar (en el ejemplo que viene a continuación he llamado a los métodos IsValid, IsFirstNameValid y IsAgeValid pero podrían tener cualquier otro nombre).
using System.ComponentModel.DataAnnotations;

namespace MyValidation.ViewModels
{
[CustomValidation(typeof(PersonCustomValidation), "IsValid", ErrorMessage = "No eres yo")]
public class PersonViewModel
{
[CustomValidation(typeof(PersonCustomValidation), "IsFirstNameValid", ErrorMessage = "Nombre no es válido")]
[Required]
[Display(Name = "Nombre")]
public string FirstName { get; set; }
[CustomValidation(typeof(PersonCustomValidation), "IsAgeValid")]
[Required]
[Display(Name = "Edad")]
public int Age { get; set; }
}

public class PersonCustomValidation
{
public static ValidationResult IsValid(PersonViewModel person)
{
if (person.FirstName == "Sergio" && person.Age == 39)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}

public static ValidationResult IsFirstNameValid(string firstName)
{
if (firstName.IndexOf(" ") == -1)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}

public static ValidationResult IsAgeValid(int age)
{
if (age > 18)
{
return ValidationResult.Success;
}
return new ValidationResult("No eres mayor de edad");
}
}
}

Lo más relevante del código es que si devolvemos un ValidationResult con errorMessage establecido a null, tomará el especificado en el atributo CustomValidation, sino prevalecerá el que hayamos pasado a ValidationResult. También es importante ver que para que se llame al método que valida la entidad como un conjunto, tienen que haberse superado con éxito todas las validaciones de propiedades, en caso contrario no se llamará.

Lógicamente el código (no sólo de éste sino de los futuros ejemplos) es mejorable. No se valida por ejemplo si el parámetro firstName es null, etc. Por eso he metido [Required], para asegurarme no tener que escribir más código del necesario en los ejemplos :)

¿Qué no me gusta de CustomValidationAttribute?

Claramente tener que hardcodear el nombre de los métodos. Sólo por eso no lo utilizaré.

ValidationAttribute
using System.ComponentModel.DataAnnotations;

namespace MyValidation.ViewModels
{
[YouAreNotMe(ErrorMessage = "No eres yo")]
public class PersonViewModel
{
[Required]
[Display(Name = "Nombre")]
[StringWithoutSpaces(ErrorMessage = "{0} no puede tener espacios")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Edad")]
[LegalAge(ErrorMessage = "No eres mayor de edad")]
public int Age { get; set; }
}

public class StringWithoutSpacesAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value.ToString().IndexOf(" ") == -1)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}
}

public class LegalAgeAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (((int)value) > 18)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}
}

public class YouAreNotMeAttribute: ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var person = (PersonViewModel) value;
if (person.FirstName == "Sergio" && person.Age == 39)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}
}
}

¿Qué me gusta de ValidationAttribute?

  • Ganamos seguridad de tipos (sobre todo comparado con CustomValidation).
  • Mejoramos la expresividad del código.
  • Cada atributo sólo tiene una responsabilidad, no es una colección de métodos estáticos en una misma clase como con CustomValidation.
  • Mantiene una simetría con el resto de validadores que vienen de serie.

En mi opinión, es una buena opción para validar propiedades pero no tanto para validar la entidad como conjunto.

IValidatableObject

Con IValidatableObject, en vez de trabajar con atributos ahora tenemos que implementar esta interface en clase de entidad que queremos validar.

No sirve para validar propiedades de forma individual, sino que está pensada para validar la entidad como un conjunto, es decir, si queremos validar propiedades tendremos que seguir optando por alguna de las opciones que lo soportan (de las expuestas todas excepto justo IValidatableObject).
public class PersonViewModel:IValidatableObject
{
[Required]
[Display(Name = "Nombre")]
[StringWithoutSpaces(ErrorMessage = "{0} no puede tener espacios")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Edad")]
[LegalAge(ErrorMessage = "No eres mayor de edad")]
public int Age { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var result = new List<ValidationResult>();
if (FirstName == "Sergio" && Age == 39)
{
result.Add(ValidationResult.Success);
return result;
}
result.Add(new ValidationResult("No eres yo"));
if (FirstName != "Sergio")
{
result.Add(new ValidationResult("Nombre no es válido",new[] {"FirstName"}));
}
if (Age != 39)
{
result.Add(new ValidationResult("Edad no es válida", new[] { "Age" }));
}
return result;
}
}

¿Qué me gusta de IValidatableObject?

  • Qué no hay que castear un parámetro para acceder a los valores de lo que estamos validando, al estar implementado en la propia clase del modelo, podemos acceder a las propiedades de la instancia sin más.
  • Qué puede devolver una lista de ValidationResult, pudiendo así afinar (si es preciso) que propiedades contienen errores con la sobrecarga de ValidationResult que espera un IEnumerable<string> memberNames.

Por ahora (y si nadie me convence de lo contrario) es mi opción preferida para validar una entidad como conjunto.

IDataErrorInfo

Con franqueza, es para mí la mayor desconocida. No la utilizado nunca y creo no la utilizaré. Es muy distinta a cualquiera de las otras aproximaciones, no sirve para validar la entidad como un conjunto (sólo para validar propiedades), obliga a devolver un mensaje (si localizas aplicaciones sabrás que esto es un problema)… pero en cualquier caso, un ejemplo sencillo servirá para completar el post y cumplir con lo prometido.
public class PersonViewModel:IDataErrorInfo
{
[Required]
[Display(Name = "Nombre")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Edad")]
public int Age { get; set; }

public string this[string columnName]
{
get
{
if (columnName == "FirstName")
{
if (FirstName.IndexOf(" ") != -1)
{
return "Nombre no es válido";
}
}
else if (columnName == "Age")
{
if (Age < 18)
{
return "No eres mayor de edad";
}
}
return null;
}
}

public string Error { get; private set; }
}

Como conclusión, a día de hoy mi resumen es el siguiente:

  • Si tengo que validar propiedades utilizo ValidationAttribute.
  • Si tengo que validar una entidad utilizo IValidatableObject.

Un saludo!

1 comentario:

  1. Qué bueno y qué currado! Me ha quedado clarísimo y sin duda me quedo con la conclusión: ValidationAttribute + IValidatableObject
    A la saca!!

    ResponderEliminar