No uses DataAnnotations en interfaces
Disclaimer
Quizás el uso de DataAnnotations en una interfaz no sea precisamente una buena práctica, así que no me aticéis por ahí porque el objetivo de esta entrada no es discutir eso, sino saltarnos esa recomendación y demostrar algunas cosas. De hecho, el uso de DataAnnotations tiene como objetivo también, evitar su uso en interfaces, siendo precisamente en su implementación donde tengamos la libertad de adoptarlas o crear nuestras propias DataAnnotations personalizadas.
Introducción
El título dicho así, puede sonar un poco drástico, pero sí,… usar DataAnnotations en nuestras interfaces no es una buena idea, y en esta entrada lejos de aceptar los axiomas que algunos creen porque otros dicen que es así y punto, voy a explicarte de forma práctica porqué digo esto.
¿Qué es o que son las DataAnnotations?
Aunque mucha gente sabe situar perfectamente DataAnnotations, me veo en la obligación de hacer una pequeña parada para indicar que son por si te pilla de nuevas o no lo tienes claro del todo.
El atributo de validación System.ComponentModel.DataAnnotations tiene como misión la validación de los campos de un modelo de datos, e incluso de una entidad si queremos.
Gracias al atributo DataAnnotations, podemos validar nuestros modelos o incluso crear nuestras propias validaciones personalizadas.
Es bastante común encontrarnos con DataAnnotations en aplicaciones web ASP.NET MVC, aunque no se nos impide para nada el uso de DataAnnotations en aplicaciones Web API, de consola o cualquier otra. De hecho, el uso de validaciones de los campos con los que va a trabajar nuestra aplicación no lo considero una opción.
Una vez introducido de forma breve lo que son DataAnnotations, vamos al cubrir de lleno el objetivo de esta entrada, que es el de explicarte por qué a mi juicio no es una buena idea utilizarlas en nuestras interfaces (por si creías que sí).
Desarrollo de un ejemplo simple con DataAnnotations
Lo primero que vamos a hacer es crear una aplicación de consola, dentro de la cual crearemos un modelo o entidad llamada Employee.
Dicha entidad tendrá dos sencillos campos. Nombre (Name) y Edad (Age).
Sobre el campo Age agregaremos una DataAnnotation que nos indicará que el valor de esta propiedad tendrá que estar comprendido entre 18 y 150.
Nuestra entidad quedará de la siguiente manera:
Employee
{
public class Employee : IEmployee
{
public string Name { get; set; }
[Range(18, 150)]
public byte Age { get; set; }
} }
La propiedad Age de nuestro modelo está decorado con una DataAnnotation de tipo Range.
Sin embargo, a la hora de validar nuestro modelo o entidad en nuestra aplicación de consola, deberemos hacer uso de una función que se encargará de validarlo entero, permitiéndonos mostrar por pantalla el campo o campos que no cumplen la condición de validación.
Este método que nos hemos creado podría bien ser el siguiente:
{
var context = new ValidationContext(@object, serviceProvider: null, items: null);
results = new List<ValidationResult>();
return Validator.TryValidateObject(@object,
context,
results,
validateAllProperties: true);
}
Y la llamada a este método desde nuestra aplicación de consola para forzar el error de validación sería:
{
Console.WriteLine("Ejecución iniciada");
Employee employee = new Employee();
employee.Name = "John";
employee.Age = 17;
var results = new List<ValidationResult>();
var modelIsValid = DataAnnotationsValidator(employee, out results);
Console.WriteLine(String.Format("¿Es el modelo válido? {0}", modelIsValid.ToString()));
if (!modelIsValid)
{
foreach (var validationResult in results)
{
Console.WriteLine(validationResult.ErrorMessage);
}
}
Console.WriteLine("Ejecución finalizada");
Console.ReadLine();
}
Nuestra aplicación en ejecución queda de la siguiente manera:
Como podemos observar, nuestra aplicación funciona tal y como esperamos.
Ampliando nuestro ejemplo
Ahora bien, de repente nos llega un requisito funcional que debemos cubrir.
Supongamos que nos han encargado tratar también los empleados externos.
Un empleado externo según los requisitos que nos han hecho llegar, tendrá los mismos campos que un usuario más el campo empresa, que indicará la empresa a la que pertenece este trabajador externo.
Aquí tenemos muchas posibilidades, pero dentro del equipo de arquitectura y del equipo técnico, entre las opciones de crear una clase abstracta, una clase base o una interfaz, o modificar el modelo para que cubra ambas opciones, se ha pensado que la mejor forma de abordar esto sea con una interface.
Alguien ha pensado incluso y después de las reuniones mantenidas con la gente de negocio (básicamente RRHH en nuestro ejemplo ficticio), que es posible que en un futuro próximo haya además de los empleados internos y los empleados externos, otros empleados,… o que quizás las propiedades nombre y edad usadas en el modelo empleado, pueda ser reutilizado por otros modelos o entidades, así que no es mala idea hacer esto (aceptamos pulpo en nuestro ejemplo).
Así que hagamos un esfuerzo más e imaginemos que nuestro proyecto no sólo posee la entidad Employee, sino que hay cerca de 50 ó más entidades o partes del modelo, y que esta interfaz nos va a venir muy bien porque de un plumazo vamos a reutilizar bastante código, y poniendo una DataAnnotation en una interfaz, resolveremos el problema de tener que poner la validación de rango en todas y cada una de las entidades. No parece mala idea… a priori…
Es decir,… nuestra interfaz y clases quedarán ahora de la siguiente manera:
IEmployee
{
public interface IEmployee
{
byte Age { get; set; }
[Range(18, 150)]
string Name { get; set; }
}
}
Employee
{
public class Employee : IEmployee
{
public string Name { get; set; }
public byte Age { get; set; }
}
}
ExternEmployee
{
public class ExternEmployee : IEmployee
{
public string Company { get; set; }
public string Name { get; set; }
public byte Age { get; set; }
}
}
Bien… ahora ejecutemos nuevamente nuestra aplicación de consola.
Obtendremos una respuesta como la que se indica a continuación:
Como podemos apreciar en la respuesta de ejecución de nuestra aplicación, el modelo o entidad Employee nos indica que es correcta.
¿Cómo es posible?.
Es evidente que la interfaz nos está indicando que la propiedad Age debe estar dentro de un rango de valores que no cumple. Pero como podemos ver, a la hora de ejecutar la acción de validación no está funcionando como esperábamos.
Modificando nuestro ejemplo
Bueno… el equipo de desarrollo se ha reunido para tratar de entender un problema que está sucediendo y que se nos ha escapado en algún momento.
Se ha dedicado bastante tiempo, un tiempo perdido al fin y al cabo, así que después todo este camino recorrido se decide cambiar la interfaz por una clase abstracta.
Esta clase abstracta es la siguiente:
EmployeeAbstract
{
public abstract class EmployeeAbstract
{
byte Age { get; set; }
[Range(18, 150)]
string Name { get; set; }
}
}
Y nuestras clases anteriores quedarán de la siguiente manera:
Employee
{
public class Employee : EmployeeAbstract
{
public string Name { get; set; }
public byte Age { get; set; }
}
}
ExternEmployee
{
public class ExternEmployee : EmployeeAbstract
{
public string Company { get; set; }
public string Name { get; set; }
public byte Age { get; set; }
}
}
Si ejecutamos nuevamente nuestro código, obtendremos un resultado como el que se indica a continuación:
Y aunque no lo voy a hacer aquí, exactamente igual ocurrirá si creamos una clase base que sea heredada por Employee y ExternEmployee.
¿Y esto por qué ocurre?.
El porqué de las cosas
La explicación de este comportamiento tenemos que buscarlo en el CLR de .NET y como funciona por dentro de acuerdo a como quiere que se comporte el equipo de .NET. No obstante, y para comprenderlo mejor, vamos a hacer alguna reflexión previa.
Las clases base son heredadas, tal y como he comentado anteriormente, es decir, los métodos, propiedades y funciones de una clase base, estarán presentes en la clase que la hereda. Por esta razón, una DataAnnotation de una clase base, estará en la clase que lo hereda, en la clase derivada.
Las interfaces por otra parte, son contratos o implementaciones a llevar a cabo, y dichas implementaciones no están presentes en la clase que implementa la interfaz hasta que esta implementación se representa como tal. Esto significa por extensión, que un metadato presente en una interfaz, NO es agregado dentro de la clase que implementa dicha interfaz. Por esta razón, si una interfaz tiene una DataAnnotation, ésta no llega a la clase que implementa esa interfaz.
Pero podemos dar una vuelta de tuerca más a este asunto. Imaginemos que tenemos una clase que implementa dos interfaces, y que estas dos interfaces poseen ambas la misma propiedad, pero con la salvedad de que una de ellas tiene una DataAnnotation y otra no, o dos DataAnnotations diferentes… ¿cual de ellas es la que tenemos que usar?.
Sin embargo, las interfaces chocan en cuanto a su implementación y uso con las clases abstractas, que son otra cosa diferente y posee la herencia como parte fundamental, algo que hemos visto, sí está soportado de forma directa.
2 Responsesso far
Muy interesante, Jorge. Saludos.
Muchas gracias Kiquenet. 😀