sábado, 2 de mayo de 2015

Validación en Entity Framework

Si es un post anterior vimos como validar el modelo de ASP.NET MVC, en este veremos cómo validar las entidades de Entity Framework antes de que persistan en la base de datos.

Que sea o no Entity Framework quien deba validar ciertas reglas de negocio antes de grabar los cambios, es una pregunta para la que no tengo respuesta, básicamente porque no sabría esgrimir un argumento sólido ni a favor ni en contra de si es una opción válida. Imagino que en aplicaciones puramente centradas en datos podría ser una opción viable, mientras que en aplicaciones orientadas al dominio, poco menos que sería un sacrilegio mover estas validaciones a la capa de persistencia. Lo dicho, me limitaré a presentar como validar entidades con Entity Framework y dejare la discusión relacionada con la arquitectura para los que realmente saben :)

Lo primero es saber que valida Entity Framework:

  • Reglas en propiedades especificadas vía DataAnnotations o FluentAPI
  • Reglas en propiedades o entidades especificadas con el atributo CustomValidationAttribute.
  • Entidades que implementan IValidatableObject.

Aquí cabe mencionar que hay ciertos atributos de validación (atributos que heredan de ValidationAttribute) que además de validar también instruyen a Code First sobre como inferir el esquema de la base de datos. Por ejemplo [Required] validará que la propiedad tiene algún valor y además creara un campo en base de datos que no admite nulos. Otro ejemplo sería [StringLength] que crearía un campo nvarchar con una determinada longitud. Si optamos por FluentAPI, IsRequired y HasMaxLength serían sus equivalentes e igualmente validarían y servirían para inferir el esquema. 

Yo personalmente prefiero utilizar FluentAPI, pero es cierto que con los atributos tenemos una oportunidad de personalizar el mensaje de error si no se supera la validación. Además, si en vez de Code First estamos utilizando Database First, sólo nos queda la opción de los atributos usando las clases buddy para agregar validación a las clases auto-generadas.

public partial class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}

[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
}

public class CustomerMetadata
{
[Required]
[StringLength(50)]
public string FirstName { get; set; }
}

Lo siguiente que debemos conocer es cuando se ejecutan las validaciones.

Bajo demanda se ejecutarán en los siguientes casos:

  • Validar una entidad llamando al método DbEntityEntry.GetValidationResult.
  • Validar una propiedad llamando al método DbPropertyEntry.GetValidationErrors.
  • Validar todas las entidades Added o Modified, llamando al método DbContext.GetValidationErrors.

Automáticamente:

  • Se validarán todas las entidades Added o Modified al llamar al método DbContext.SaveChanges (siempre y cuando Configuration.ValidateOnSaveEnabled sea true, que es su valor por defecto).

Validar una entidad con DbEntityEntry.GetValidationResult devuelve un objeto del tipo DbEntityValidationResult con 3 propiedades:

  • IsValid, indica si la entidad es válida.
  • ValidationErrors, colección de DbValidationError con propiedades PropertyName y ErrorMessage
  • Entry, entidad que está siendo validada.

La propiedad Entry podría parecer redundante (de hecho en el caso de DbEntityEntry.GetValidation lo es, ya sabemos cual es la entidad que estamos validando) pero en otros casos como en SaveChanges nos servirá para saber a que entidad nos estamos refiriendo cuando iteremos sobre un conjunto de entidades.

También es importante ver no es necesario que la entidad esté atachada al contexto para lanzar la validación.

var customer = new Customer();
DbEntityValidationResult validationResult = context.Entry(customer).GetValidationResult();
if (!validationResult.IsValid)
{
Console.WriteLine("La entidad de tipo {0} no es válida.", validationResult.Entry.GetType().Name);
foreach (DbValidationError validationError in validationResult.ValidationErrors)
{
Console.WriteLine("Propiedad {0}", validationError.PropertyName);
Console.WriteLine("Mensaje {0}", validationError.ErrorMessage);
}
}

En el caso de querer validar una propiedad con DbPropertyEntry.GetValidationErrors, lo que nos devolverá este método es directamente una colección de objetos del tipo DbValidationError.

var customer = new Customer();
ICollection<DbValidationError> validationErrors = context.Entry(customer).Property(p=>p.FirstName).GetValidationErrors();
if (validationErrors.Any())
{
foreach (DbValidationError validationError in validationErrors)
{
Console.WriteLine("Propiedad {0}", validationError.PropertyName);
Console.WriteLine("Mensaje {0}", validationError.ErrorMessage);
}
}

La validación con DbContext.GetValidationErrors y con DbContext.SaveChanges es muy similar, ambas validan entidades Added o Modified. La diferencia está en que DbContext.GetValidationErrores devuelve una colección de DbEntityValidationResult, mientras que SaveChanges lanza una excepción del tipo DbEntityValidationException.

var customer = new Customer();
context.Customers.Add(customer);

try
{
context.SaveChanges();
}
catch (DbEntityValidationException ex)
{
foreach (DbEntityValidationResult entityValidationError in ex.EntityValidationErrors)
{
Console.WriteLine("Entidad {0}", entityValidationError.Entry.Entity.GetType());
Console.WriteLine("Estado {0}", entityValidationError.Entry.State);
foreach (var validationError in entityValidationError.ValidationErrors)
{
Console.WriteLine("Propiedad {0}", validationError.PropertyName);
Console.WriteLine("Mensaje {0}", validationError.ErrorMessage);
}
}
}

Qué solo valide entidades Added o Modified podemos personalizado si sobrescribimos el método ShouldValidateEntity de DbContext.

Otro punto a tener en cuenta es que cuando se llama a DbEntityEntry.GetValidationResult (ya sea directamente o como consecuencia de haber llamado a DbContext.GetValidationErrors o DbContext.SaveChanges) no tendremos que preocuparnos por efectos no deseados con LazyLoading puesto que se desactivará automáticamente durante el proceso de validación.

Si hablamos de validar entidades como conjunto, hemos visto que podemos hacerlo con CustomValidationAttribute o IValidatableObject (y cabe recordar que esta validaciones a nivel de entidad sólo se ejecutaran si se superan las validaciones a nivel de propiedad), pero en ninguno de los casos expuestos hasta ahora hemos tenido acceso al contexto durante la validación, luego ¿cómo validar una entidad si tenemos que validar también datos en otras entidades del contexto que no son navegables a partir de la entidad que estamos validando o incluso tenemos que validar contra datos ya existentes en la base de datos? Pues sobrescribiendo el método ValidateEntity de DbContext donde ya sí tenemos acceso al contexto.

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
return base.ValidateEntity(entityEntry, items);
}

Las estrategias posibles aquí son dos (según he podido ver), una es utilizar la variable items que es un diccionario que se pasará a IValidatableObject. De este modo podríamos pasar el propio contexto y así en método Validate tenerlo disponible. Otra opción es llevar a cabo directamente la validación en el propio método ValidateEntity (llamando finalmente a base.ValidateEntity(entityEntry, items) si resulta oportuno).

public class ConsoleApplication1DbContext : DbContext
{
public DbSet<Customer> Customers { get; set; }

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
items["context"] = this;
return base.ValidateEntity(entityEntry, items);
}
}

public class Customer : IValidatableObject
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var context = (DbContext)validationContext.Items["context"];
//do something...
return Enumerable.Empty<ValidationResult>();
}
}

Un saludo!