domingo, 23 de junio de 2013

Un poquito más de AutoMapper, por favor

Qué es AutoMapper y una introducción al mismo está explicado perfectamente en el post AutoMapper (I) Flattening de Luis Ruiz Pavón. Asumiendo que ya te has leído ese post (si no lo has hecho, corre a leerlo y luego vuelve aquí), en este post lo que quiero es contar algunos otros conceptos que podrían sernos de utilidad a la hora de realizar esa tediosa e ingrata tarea que es la de mapear objetos.

Partiendo de estos tipos, iremos viendo distintos usos de AutoMapper:

class Jugador

{

    public string Nombre { get; set; }

    public int Dorsal { get; set; }

    public string Posicion { get; set; }

}

 

class Jugador2

{

    public string Nombre { get; set; }

    public int Dorsal { get; set; }

}

En su versión más básica, sólo son necesarios los siguientes pasos para utilizar AutoMapper:

  • Crear un mapa donde especificaremos un tipo origen y un tipo destino.
    • Sin mapa no hay conversión posible.
    • Aquí podría ayudarnos el método ReverseMap para no tener que crear 2 mapas sino con uno sólo crear tanto el mapa de A a B como el mapa de B a A
  • Mapear un objeto origen a un tipo destino.

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>();

var jugador = new Jugador()

    {

        Nombre = "Sergio",

        Dorsal = 10,

        Posicion = "Delantero"

    };

var jugador2 = AutoMapper.Mapper.Map<Jugador, Jugador2>(jugador);

Ahora jugador2 tiene los siguientes datos:

image

Como vemos, el comportamiento predeterminado de AutoMapper es encontrar propiedades coincidentes por el nombre y asignarlas. Nada más y nada menos. En realidad, esto responde al nombre de Flatenning, pero lo dicho… en el post AutoMapper (I) Flattening está muy bien explicado y con casos más avanzados.

Aunque luego veremos que es configurable, en principio si una propiedad existe en origen y no en destino o viceversa, no pasa nada, simplemente se pasa por alto (en nuestro ejemplo anterior la propiedad Posicion existe en Jugador pero no en Jugador2).

Algo obvio si leemos la documentación (yo no hago y así me va) es que distintas sobrecargas del método Map tienen distintos comportamientos. Así por ejemplo Mapper.Map<T, T1>(instancia) devuelve una nueva instancia con el resultado del mapeo mientras que, Mapper.Map(instancia1, instancia2) mapea de una instancia a otra. Avisado quedas.

Otro punto muy importante de AutoMapper es el módulo de Projection. Con la proyección podremos configurar ciertos aspectos del mapeo que nos ayudarán a resolver casos concretos. Por ejemplo, si cambiamos el tipo de Jugador.Dorsal a String, todo seguirá funcionando porque AutoMapper sabe convertir de String a int (el tipo destino en Jugador2.Dorsal). Sin embargo, si ahora asignamos a Jugador.Dorsal algo que no sea un número, AutoMapper fallará (lógicamente no habrá podido convertir ese valor a número):

var jugador = new Jugador()

    {

        Nombre = "Sergio",

        Dorsal = "Sin dorsal",

        Posicion = "Delantero"

    };

var jugador2 = AutoMapper.Mapper.Map<Jugador, Jugador2>(jugador);

image

Para solucionar esta situación tenemos que utilizar la proyección. Por ejemplo, podemos utilizar el método Condition para evaluar si la propiedad Dorsal tiene o no que mapearse:

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>().

    ForMember(p => p.Dorsal, opt => opt.Condition(j =>

        {

            int result;

            return int.TryParse(j.Dorsal, out result);

        }));

Ahora “10” se mapeará pero “Sin dorsal” no se mapeará.

Otra opción sería utilizar el método Ignore para expresar que aunque coincida el nombre no queremos mapearlo nunca:

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>().

    ForMember(p => p.Dorsal, opt => opt.Ignore());

Quizás Dorsal siempre sea un valor fijo, en ese caso utilizaremos UseValue:

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>().

            ForMember(p => p.Dorsal, opt => opt.UseValue(99));

Otra opción es que sin Dorsal no es numérico devolvamos 99 pero si es numérico devolvamos el mismo Dorsal. Ahora utilizaremos el método MapFrom:

static bool IsNumeric(string value)

{

    int result;

    return int.TryParse(value, out result);

}

 

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>().

            ForMember(p => p.Dorsal, opt => opt.MapFrom(j =>

                IsNumeric(j.Dorsal) ? Convert.ToInt32(j.Dorsal) : 99));

También podemos encadenar llamadas para un mismo miembro puesto que AutoMapper es una API fluida. Por ejemplo, si Dorsal es válido sumarle 10:

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>().

    ForMember(p => p.Dorsal, opt => opt.Condition(j =>

    {

        int result;

        return int.TryParse(j.Dorsal, out result);

    })).

    ForMember(p => p.Dorsal, opt => opt.MapFrom(j =>

        Convert.ToInt32(j.Dorsal) + 10));

Como podrás imaginar hay un montón de funciones para configurar el mapeo. A mí personalmente me parecen interesantes las funciones BeforeMap y AfterMap que permite ejecutar código antes y después de que AutoMapper haya hecho el mapeo:

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>().

    ForMember(p => p.Dorsal, opt => opt.Ignore()).

    AfterMap((j, j2) =>

    {

        j2.Dorsal = 99;

    });

Otra opción cuando se complica el mapeo de un cierto valor es crear un Custom Value Resolver para configurar el valor destino. Para ello crearemos una clase que herede de la clase abstracta ValueResolver<TSource, TDestination> y sobrescribiremos el método ResolveCore, después y en el mapa configuraremos que utilice este nuevo Custom Value Resolver para resolver el valor de un miembro:

public class MyCustomValueResolver : ValueResolver<Jugador, int>

{

    protected override int ResolveCore(Jugador source)

    {

        int result;

        if (int.TryParse(source.Dorsal, out result))

        {

            return result;

        }

        return 99;

    }

}

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>().

    ForMember(p => p.Dorsal, opt => opt.ResolveUsing<MyCustomValueResolver>());

Además de Custom Value Resolvers también existe el concepto de Custom Type Converters que sirven para configurar como se mapeará de un tipo a otro. Por ejemplo, quizás queramos que en nuestros DTO/ViewModels las fechas tengan un formato concreto o simplemente queremos tomar el control absoluto del mapeo de Jugador a Jugador2. En cualquier caso, será necesario crear una clase que implemente la interface ITypeConverter<TSource, TDestination> y después utilizar el método ConvertUsing para especificar a AutoMapper como queremos realizar el mapeo:

public class MyCustomTypeConverter : ITypeConverter<Jugador, Jugador2>

{

    public Jugador2 Convert(ResolutionContext context)

    {

        var jugador = (Jugador)context.SourceValue;

        var jugador2 = new Jugador2

            {

                Nombre = jugador.Nombre

            };

        int result;

        if (int.TryParse(jugador.Dorsal, out result))

        {

            jugador2.Dorsal = result;

        }

        return jugador2;

    }

}

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>().

    ConvertUsing<MyCustomTypeConverter>();

Para terminar, me gustaría volver a hablar sobre el tratamiento de las propiedades que existen en destino pero no en origen. Inicialmente AutoMapper no se quejará si una propiedad de destino no existe en origen. Sin embargo, existe el método AssertConfigurationIsValid que está entendido para ser utilizado dentro un test unitario y validar la configuración. Validar la configuración sí se quejará si una propiedad de destino no existe en origen (siempre y cuando no se haya ignorado o se haya hecho algún otro tratamiento especial con la propiedad). Por ejemplo, si agregamos una propiedad Reserva (bool) a Jugador2 y ejecutamos AssertConfigurationIsValid, ahora tendremos el siguiente error porque esta propiedad no existe en la clase Jugador.

Unmapped members were found. Review the types and members below.

Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type

==================================================

Jugador -> Jugador2 (Destination member list)

PostAutoMapper.Jugador -> PostAutoMapper.Jugador2 (Destination member list)

--------------------------------------------------

Reserva


Si optamos por validar la configuración podríamos utilizar un método de extensión llamado
IgnoreAllNonExisting que hace que todas las propiedades que no existen en origen pero sí en destino sean ignoradas.

AutoMapper.Mapper.CreateMap<Jugador, Jugador2>().

    IgnoreAllNonExisting();

Por último, si quieres organizar mejor los mapas (porque por ejemplo quieres centralizarlos o reutilizarlos) la mejor idea es seguir la convención propuesta por el mismo AutoMapper https://github.com/AutoMapper/AutoMapper/wiki/Configuration

Un saludo!

LazyLoading en Code First

Aunque normalmente preferimos desactivar la carga diferida en los proyectos de Entity Framework, hemos decidido activarla en el proyecto actual en que el estamos trabajando.

Uno de los principales motivos por el que desactivamos la carga diferida es que exige conocer exactamente cómo se comportará Entity Framework, o sino dará lugar a escenarios descontrolados donde el acceso a propiedades de navegación de las entidades puede derivar en un exceso de consultas a la base de datos, sin que el programador sea plenamente consciente de ello.

En cualquier caso, la carga diferida no es mala por sí misma, simplemente un mal uso puede derivar en un pésimo rendimiento pero, por el contrario, un buen uso puede ayudarnos enormemente a la hora de realizar nuestros programas.

Lo primero es saber cómo activar o desactivar la carga diferida. Para que funcione la carga diferida, Entity Framework tiene que poder crear proxys dinámicos en tiempo de ejecución que heredarán de nuestras clases POCO y sobrescribirán las propiedades de navegación (de referencia y de colección) para cargar automáticamente los datos relacionados en el primer acceso a la propiedad. Para ello, una clase de entidad tiene que cumplir los siguientes requerimientos:

  • Ser pública y no sellada.
  • Alguna propiedad de navegación tienen que ser marcada como virtual.
    • Aquí he dicho “alguna” con todo el conocimiento de causa, es decir, si al menos una propiedad de navegación es virtual se creará el proxy dinámico, sino no. Esto también significa que podemos tener en la misma entidad propiedades de navegación que funcionarán con la carga diferida y otras no.

Además también es necesario establecer a true las propiedades LazyLoadingEnabled y ProxyCreationEnabled de la configuración del contexto (que por defecto ya están activas).

Como lo más sencillo para todos es ver código, aquí va el necesario para crear una base de datos de Equipos y Jugadores con algunos datos iniciales y sobre la que trabajaremos para todos los ejemplos:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
 
namespace LazyLoading
{
    class Program
    {
        static void Main(string[] args)
        {
            const string nameOrConnectionString =
                "Data Source=(local);Initial Catalog=Prueba;Integrated Security=SSPI;MultipleActiveResultSets=True";
            Database.SetInitializer(new CreateDatabaseIfNotExistsWithSeedData());
            using (var context = new PruebaContext(nameOrConnectionString))
            {
                context.Database.Initialize(false);
                Console.WriteLine("Pulse una tecla para continuar...");
                Console.ReadKey();
            }
        }
    }
 
    class CreateDatabaseIfNotExistsWithSeedData : CreateDatabaseIfNotExists<PruebaContext>
    {
        protected override void Seed(PruebaContext context)
        {
            var realMadrid = new Equipo();
            realMadrid.Nombre = "Real Madrid";
            realMadrid.Jugadores = new HashSet<Jugador>();
            realMadrid.Jugadores.Add(new Jugador() { Nombre = "Cristiano" });
            realMadrid.Jugadores.Add(new Jugador() { Nombre = "Iker" });
            context.Equipos.Add(realMadrid);
 
            var barcelona = new Equipo();
            barcelona.Nombre = "Barcelona";
            barcelona.Jugadores = new HashSet<Jugador>();
            barcelona.Jugadores.Add(new Jugador() { Nombre = "Messi" });
            context.Equipos.Add(barcelona);
 
            var atleticoMadrid = new Equipo() { Nombre = "Atlético de Madrid" };
            context.Equipos.Add(atleticoMadrid);
        }
    }
 
    class PruebaContext : DbContext
    {
        public DbSet<Equipo> Equipos { get; set; }
        public DbSet<Jugador> Jugadores { get; set; }
 
        public PruebaContext(string nameOrConnectionString)
            : base(nameOrConnectionString)
        {
        }
    }
 
    [Table("Jugadores")]
    public class Jugador
    {
        public int JugadorId { get; set; }
        public string Nombre { get; set; }
        public int EquipoId { get; set; }
        public virtual Equipo Equipo { get; set; }
    }
 
    [Table("Equipos")]
    public class Equipo
    {
        public int EquipoId { get; set; }
        public string Nombre { get; set; }
        public virtual ICollection<Jugador> Jugadores { get; set; }
    }
}

Algo importante que hay que conocer es cuando los proxies serán creados y cuando no. Entity Framework sólo creará proxys dinámicos para los objetos materializados en el contexto a través de una consulta y para objetos creados manualmente a través del método DbSet.Create.

Una vez ya tenemos la base de datos creada y con datos y hemos cumplido los requerimientos para la creación de proxies, veamos como sucede la magia de LazyLoading y algunos tips al respecto:

   1: var realMadrid = context.Equipos.Single(p => p.Nombre.Equals("Real Madrid"));
   2: foreach (var jugador in realMadrid.Jugadores)
   3: {
   4:     Console.WriteLine(jugador.Nombre);
   5: }

En este código, primero recuperamos un equipo y después iteramos sobre los jugadores del mismo. Lo que permite LazyLoading es precisamente iterar sobre los jugadores del equipo sin tener que preocuparnos de si están o no cargados en memoria. Es decir, en el primer acceso a la propiedad de navegación Jugadores, EF cargará automáticamente los datos desde la base de datos. Esto significa que se habrán ejecutado 2 SELECT, la primera estaba clara y sucedió al recuperar el equipo, sin embargo la segunda fue transparente para nosotros y es justamente esto la bondad y la maldad de LazyLoading ¿Sabías que esto iba a suceder? Entonces no hay problema, lo tenías previsto ¿No sabías que esto iba a suceder? Pues entonces hay un acceso a base de datos en tu código que podría arruinar el rendimiento de tu aplicación, depende de en que escenarios.

Asumiendo que todos conocemos ya cómo funciona LazyLoading y sabremos reconocer escenarios malignos SELECT N+1, quedan algunos trucos que podemos utilizar para paliar esta situación si es necesario:

   1: var realMadrid = context.Equipos.Single(p => p.Nombre.Equals("Real Madrid"));
   2: var entry = context.Entry(realMadrid);
   3: if (!entry.Collection(p => p.Jugadores).IsLoaded)
   4: {
   5:     entry.Collection(p => p.Jugadores).Load();
   6: }
   7: foreach (var jugador in realMadrid.Jugadores)
   8: {
   9:     Console.WriteLine(jugador.Nombre);
  10: }

En este código, en la línea 3 investigamos si está o no cargada la colección de Jugadores. En caso de no estarlo, la cargamos de forma explícita en la línea 5. Ahora volvemos a tener controlado el escenario.

Lógicamente también sería válido utilizar carga temprana (eager loading) y así recuperar todos los datos con una sola consulta en base de datos. Aunque la carga temprana podría parecer la solución definitiva, más te valdría echar un vistazo al profiler de SQL y confirmar que la sentencia ejecutada no es un SELECT con chorrocientos JOIN que más que una consulta, podría ser una aberración que matara igualmente tu aplicación.

   1: var realMadrid = context.Equipos.
   2:     Include(p=>p.Jugadores).
   3:     Single(p => p.Nombre.Equals("Real Madrid"));
   4: foreach (var jugador in realMadrid.Jugadores)
   5: {
   6:     Console.WriteLine(jugador.Nombre);
   7: }

Otra situación que podría darse es que no siempre podemos tener la certeza de que las propiedades de navegación serán referencias válidas. Veamos el siguiente ejemplo:

   1: var equipo = context.Equipos.Create();
   2: equipo.Nombre = "Sevilla";
   3: context.Equipos.Add(equipo);
   4: var jugador = context.Jugadores.Create();
   5: jugador.Nombre = "Negredo";
   6: equipo.Jugadores.Add(jugador);
   7: context.SaveChanges();

En la línea 6 asumimos que Jugadores es una referencia válida, pero sin embargo al ejecutar el código tenemos la siguiente excepción:

clip_image001[4]

Esto ocurre porque la entidad equipo aún no está grabado en la base de datos.Para solucionar esto tenemos 2 posibilidades:

  • Grabar el contexto antes de la línea 6
  • Igualmente antes de la línea 6 crear manualmente la colección de Jugadores.

Yo personalmente me inclino siempre por la segunda solución que implica un menor número de accesos a la base de datos:

equipo.Jugadores = new HashSet<Jugador>();
var jugador = context.Jugadores.Create();

Estoy seguro que utilizar LazyLoading será un problema para algunos e incluso lo demonizarán, pero yo creo que al fin y al cabo es una característica más de Entity Framework, que depende de en qué escenarios podría resultarnos de mucha ayuda. La decisión queda en tus manos.

Un saludo!

jueves, 20 de junio de 2013

Tips para comprobar el modelo de Code First en Sql Server

Yo no sé tú, pero yo con Code First siempre tengo que terminar echando un vistazo a la base de datos para confirmar que las relaciones declaradas en el modelo se han inferido correctamente y se han generado como tenía previsto.

Sin duda, la mayor parte de las veces lo que hago es ver si las relaciones han implementado o no la eliminación en cascada y también ver si Code First me ha generado mágicamente algún campo del tipo Entidad_CampoClavePrincipalId para soportar una relación (es decir, no ha inferido correctamente la propiedad de clave externa a partir de mi modelo).

Pues nada, un par de tips rápidos por si quieres comprobar esto en un Sql Server:

Para ver que relaciones tienen cascada:

SELECT  *

FROM    INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS

WHERE   DELETE_RULE = 'CASCADE'

Para ver que campos coinciden con Texto_Texto:

SELECT  *

FROM    INFORMATION_SCHEMA.COLUMNS

WHERE   COLUMN_NAME LIKE '%[_]%'

Un saludo!