La técnica de los atributos personalizados de .NET nos permite asociar metadatos e información a nuestras entidades de código, sean clases, propiedades, métodos, etc… Cuando asociamos un atributo a una entidad de código, podemos luego leerla de nuevo utilizando otra técnica llamada Reflection.

Explicar la técnica Reflection podría tomar varios artículos, pero intentaré mostrar y explicar los puntos básicos de la misma para poder usar los atributos personalizados sin tener que ser un “guru” de Reflection. Si quieres ver Reflection a fondo, aquí tienes su enlace en la MSDN.

Para este artículo he preparado un proyecto de consola totalmente funcional en C# que implementa todas las cosas necesarias para mapear valores de una clase a otra sin tener que asignar cada propiedad manualmente.

Si eres del estilo descarga y prueba, puedes comenzar descargando el proyecto de mi repositorio Bitbucket usando estos enlaces:

Repositorio Git: http://repo.joanvilarino.info/attributemappersample

Descargar ZIP: http://repo.joanvilarino.info/attributemappersample/downloads

Pero yo te recomiendo que sigas leyendo para una mejor explicación de lo que está pasando! De manera que, sin más introducción por mi parte… manos a la obra!

Utilidad de los atributos personalizados

A lo mejor has estado usando atributos personalizados sin saberlo, o sabiéndolo, pero sin pensar que tú podrías hacer tus propios atributos

Por ejemplo, si has hecho algo con MVC, estarás acostumbrado a esto:

[HttpPost]
public ActionResult PostMethod() {
}

O, a lo mejor, cuando has necesitado serializar objetos, has definido tus clases de esta manera:

[System.Serializable]
public class MySerializableClass {
}

Bien, éstos son solo algunos de los atributos que .NET tiene ya incluidos. Vamos a darles un pequeño giro creando algunos propios para el mapeador.

StaticMapper

El ejemplo que vamos a ver en éste artículo, utiliza los atributos para crear una clase StaticMapper que nos permitirá copiar valores entre entidades. Es una solución muy útil en escenarios donde, por ejemplo, tienes entidades planas de base de datos, o de un repositorio externo (POCO entities) y quieres copiarlas a tus propios modelos.

Creando nuestros atributos

Para crear tus propios atibutos, todo lo que necesitas es definir clases para ellos, heredando de la clase System.Attribute.

En nuestro proyecto de ejemplo, vamos a utilizar dos atributos personalizados:

  • MappableClassAttribute Este atributo marca la clase como “mapeable” para nuestro mapeador, y le dice cual es su clase gemela.
  • MappedPropertyAttribute Utilizaremos este atributo para definir a nivel de propiedad, cuales tienen que copiarse y cualquier aspecto individual del mapeo.
  • Aquí está el código para ambas:

    [AttributeUsage(AttributeTargets.Class)]
    public class MappableClassAttribute : Attribute
    {
        private readonly Type _associatedType;
        public Type AssociatedType { get { return _associatedType; } }
    
        public MappableClassAttribute(Type sourceAndTargetType)
        {
            _associatedType = sourceAndTargetType;
        }
    }
    
    [AttributeUsage(AttributeTargets.Property)]
    public class MappedPropertyAttribute : Attribute
    {
        public string MapTo { get; set; }
    }
    

    Lo primero de todo, te habrás dado cuenta del atributo AttributeUsage en ambas definiciones (atributos en las clases atributo, ¿no es divertido?).

    AttributeUsage restringe dónde puedes utilizar éste nuevo atributo. No es obligatorio, pero si el nuevo atributo que estas creando está pensado solamente para algunos tipos de entidad (clase, método, propiedad, etc…) es una buena idea restringirlo, y el compilador entonces nos dará errores si lo usamos en un sitio incorrecto.

    He usado dos maneras diferentes de implementar los nuevos atributos. La primera de ellas es usando un constructor para inicializar la propiedad AssociatedType, mientras que en la segunda la clase no tiene un constructor y tiene una auto-propiedad pública.

    Esta diferencia se aplica a la hora de usarlos. Para usar el primer atributo lo haremos de ésta manera:

    [MappableClass(typeof(MyPOCOEntity))]
    public class MyViewModel {
       ....
    }
    

    Puedes ver que no hay nombre de propiedad implicado para pasarle el valor, ya que usará el constructor primario que hemos definido al implementar la clase, mientras que en el segundo caso es diferente:

       ...
       [MappedProperty(MapTo = "MyPropertyPOCO")]
       public string MyPropertyVM { get; set; }
       ...
    

    En esta declaración, estoy obligado a usar el nombre de propiedad que inicializo porque el atributo no tiene un constructor definido.

    Como regla base, definirás constructores para atributos cuando la propiedad a inicializar sea obligatoria, y dejarás fuera del constructor las que no lo sean. En caso de no tener propiedades obligatorias, simplemente no definirás constructor.

    [MyAttrib("ForcedParam", OtherParam = true]
    

    Usando los atributos en nuestra clase modelo

    Bien, ya tenemos nuestros atributos, así que ya es momento de utilizarlos en nuestra clase modelo. Para este ejemplo, nuestra clase modelo va a ser casi una copia idéntica a la entidad POCO o de base de datos, aunque no tiene por qué ser así. Nuestra única diferencia va a ser que la propiedad DtoId se va a llamar Id en nuestro modelo, con lo cual habrá que indicarlo en nuestro atributo mediante el uso de la propiedad MapTo.

    Esta es la entidad de base de datos o entidad POCO:

    public class TestDto
    {
        public long DtoId { get; set; }
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
    }
    

    Y aquí está nuestro modelo incluyendo ya los atributos:

    [MappableClass(typeof(TestDto))]
    public class TestViewModel
    {
        [MappedProperty(MapTo = "DtoId")]
        public long Id { get; set; }
        [MappedProperty]
        public string Name { get; set; }
        [MappedProperty]
        public DateTime BirthDate { get; set; }
    }
    

    Hey! ¿Que ha pasado con la parte final ...Attribute de nuestros atributos pesonalizados? Bien, por convención las clases atributo siempre acaban con la palabra Attribute, y .NET lo sabe y ya busca la clase por su nombre correcto, aunque si pones el nombre completo, funcionará de todas maneras.

    Mirando este código es facil darse cuenta de que estamos creando una clase TestViewModel que puede mapearse con la clase TestDto, y que sus propiedades son mapeables tambien. Solo hemos tenido que especificar el parámetro MapTo para la propiedad que no tiene el mismo nombre en ambas. Si dejamos ese parámetro en blanco, nuestra clase mapeadora asumirá que es el mismo nombre.

    Leyendo los atributos con Reflection

    Tenemos nuestros atributos en su sitio, pero eso sería inútil si no pudiésemos leerlos de nuevo. ¿Como hacemos eso? Uando Reflection.

    Reflection es una técnica que .NET implementa para permitirnos acceder a las declaraciones y estructura del código en tiempo de ejecución. Esto es: puedo usar una instancia de un objeto para comprobar de qué tipo es, su herencia, sus propiedades, campos, métodos, constructores… vamos, cualquier cosa.

    Para acceder a esa información usaremos el tipo, al cual accedemos si es un objeto con miObjeto.GetType() o si es un tipo o clase, con typeof(miClase). Una vez tenemos el tipo, ya podremos acceder a toda la información que nos dá Reflection:

    PropertyInfo[] propsInfo = myObject.GetType().GetProperties();
    MethodInfo methodInfo = myObject.GetMethod("MyClassMethod");
    FieldInfo[] fieldsInfo = typeof(MyClass).GetFields();
    

    Todos las clases de información de reflection (PropertyInfo, MethodInfo, etc…) tienen métodos para obtener a los atributos pesonalizados del elemento. Los más usados son los siguientes:

    • object[] GetCustomAttributes(inherit) Obtiene todos los atributos personalizados del elemento.
    • object[] GetCustomAttributes(type, inherit) Obtiene todos los atributos personalizados del tipo type.
    • T GetCustomAttribute(inherit) Obtiene el atributo del tipo T si lo hay, y si no se ha definido para ese elemento, nos devuelve null.

    Si usamos alguna de las funciones no genéricas, necesitaremos castear el objeto a nuestra clase de atributo personalizado:

    var myAttr = myObj.GetType().GetProperty("MyProp")
                   .GetCustomAttributes(typeof(MyAttribute),false)
                   .FirstOrDefault() as MyAttribute;
    if (myAttr != null) {
       .....
    }
    

    Mapeando

    Aquí está parte de la implementación de la clase StaticMaper, donde comprobamos si un tipo es mapeable desde otro:

    public static bool IsMappableWith(this Type type, Type targetType)
    {
        // Uno de los tipos debe tener MappableClassAttribute apuntando al  
        // otro. Si no es así, no podemos mapearlos!
        var attrSource = type.GetCustomAttribute<MappableClassAttribute>(false);
        var attrTarget = targetType.GetCustomAttribute<MappableClassAttribute>();
        return (attrSource != null && attrSource.AssociatedType == targetType)
            || (attrTarget != null && attrTarget.AssociatedType == type);
    }
    

    Y éste es el método principal que se encarga del mapeo en sí:

    public static TOut MapTo<TOut, TIn>(this TIn objIn, TOut objOut) 
        where TIn : class 
        where TOut : class 
    {
        // Son clases mapeables entre si?
        if (!objIn.GetType().IsMappableWith(typeof(TOut)))
            throw new FormattedException("The types '{0}' and '{1}' are not mappable.",
                objIn.GetType().Name, objOut.GetType().Name);
    
        Type infoType = null;
        // La clase que contiene el atributo es la del objeto "In" o "Out" ?
        var attr = objIn.GetType().GetCustomAttribute<MappableClassAttribute>();
        if (attr != null)
            infoType = objIn.GetType();
        else
        {
            attr = typeof (TOut).GetCustomAttribute<MappableClassAttribute>();
            if (attr != null)
                infoType = typeof (TOut);
        }
    
        // Iteramos las propiedades de la clase que define el mapeo buscando las que
        // llevan atributo de mapeado
        infoType.GetProperties()
            .Where(p => p.GetCustomAttributes(typeof(MappedPropertyAttribute), false).Any())
            .ToList()
            .ForEach(p => MapValue(infoType, p, objIn, objOut));
    
        return objOut;
    }
    

    Finalmente, el método MapValue se encarga de leer y asignar los valores de las propiedades de un objeto al otro, llamando a Map para copiar el valor con seguridad:

    private static void MapValue(Type infoType, PropertyInfo prop, object objIn, object objOut)
    {
        // Obtenemos nuestro atributo personalizado
        var attr = prop.GetCustomAttribute<MappedPropertyAttribute>();
        // Comprobar si la clase que define el mapeo es objIn o objOut, para saber
        // la dirección del mismo... fijar souce o target y llamar a Map para copiar
        // el valor
        if (infoType == objOut.GetType())
        {
            var sourceProp = objIn.GetType().GetProperty(attr.MapTo ?? prop.Name);
            if (sourceProp != null)
                Map(sourceProp, objIn, prop, objOut);
        }
        else
        {
            var targetProp = objOut.GetType().GetProperty(attr.MapTo ?? prop.Name);
            if (targetProp != null)
                Map(prop, objIn, targetProp, objOut);
        }
    }
    
    private static void Map(PropertyInfo pIn, object objIn, PropertyInfo pOut, object objOut)
    {
        // Los tipos deben ser compatibles...
        if (!pOut.PropertyType.IsAssignableFrom(pIn.PropertyType))
            throw new FormattedException("Can't map value from '{0}' ({1}) to '{2}' ({3}"
                , pIn.Name, pIn.PropertyType.Name, pOut.Name,pOut.PropertyType.Name);
        // Asignamos el valor
        try
        {
            pOut.SetValue(objOut,pIn.GetValue(objIn,null));
        }
        catch (Exception ex)
        {
            throw new FormattedException("Error converting data from {0} to {1}", ex, pIn.Name, pOut.Name);
        }
    }
    

    Ejecutando

    Todo listo! preparemos nuestro código de prueba y ejecutemos! debería ser algo parecido a esto:

    Console.WriteLine("{0}.IsMappableWith({1})         : {2}",
        typeof(TestViewModel).Name, 
        typeof(TestDto).Name, 
        typeof(TestViewModel).IsMappableWith(typeof(TestDto)));
    
    var dto = new TestDto()
    {
        DtoId = 20,
        BirthDate = DateTime.Today,
        Name = "TestSubject"
    };
    Console.WriteLine("Sample object               : {0} / {1} / {2}"
        , dto.DtoId, dto.Name, dto.BirthDate);
    
    var vm = dto.MapTo<TestViewModel>();
    Console.WriteLine("TestDTo > TestViewModel     : {0} / {1} / {2}",
         vm.Id, vm.Name,vm.BirthDate);
    
    vm.Name += "(Modified)";
    Console.WriteLine("Changed name to '{0}'.", vm.Name);
    
    var newDto = vm.MapTo<TestDto>();
    Console.WriteLine("TestViewModel > new TestDto : {0} / {1} / {2}",
        newDto.DtoId,newDto.Name,newDto.BirthDate);
    

    Y el resultado debería ser este:
    mapper

    Gracias!

    Más material en breve! Espero que hayas encontrado el artículo interesante. Si es así, lo de siempre, Like, Share, Facebook, Twitter, Google Plus, LinkedIn y esas cosas.

    Si quieres ver más ejemplos de código, visita mi repositorio en http://repo.joanvilarino.info

    Follow me

    Joan Vilariño

    Senior .NET Developer at Ohpen
    Follow me