Mapear objetos en C#

En test post voy a hablar de diferentes librerías que tenemos para hacer el map de una clase a otra en C#.

 

1 - Que es hacer un mapeo? 

Bueno hacer un map, o un mapeo, o como lo quieras llamar no es ni más ni menos que coger un objeto, y convertirlo en otro diferente. 

En C#, así como en muchos otros lenguajes, tenemos librerías que nos permite hacer dicho mapeo de forma automática, ya que por defecto no se puede. 

 

En este post vamos a ver dos, pero, si no estáis familiarizados, que sepáis que hay decenas de librerías.

 

 

2 - Librería Automapper en C#

Automapper es posiblemente la librería de mapeo más famosa dentro de C#. Esta librería se crea, básicamente para permitirnos a los desarrolladores no tener que realizar el mapeo a mano, ya que es una tarea muy tediosa y aburrida.

 

Por ejemplo cuando tenemos DTOs y entidades u objetos de dominio tenemos que mapear prácticamente 1 a 1 dichos objetos, y como digo, es aburrido y cansino.

Así que por esto nace y se hace popular automapper. 

 

2.1 - Ejemplos y casos de uso de automapper en C#

Para ver un ejemplo, el caso de uso más sencillo de automapper es mapear Tipo A a Tipo B y por defecto prácticamente no necesitamos configuración.

  • Nota: si las propiedades se llaman igual, se mapean correctamente sin configuración

 

En nuestro caso, tenemos dos tipos, el DTO y la Entidad:

public record UserDto(string UserName, string Email);    
public record UserEntity(string UserName, string Email);

Y ahora simplemente vamos a instanciar nuestro mapper y utilizarlo, así podemos ver que funciona:

[Fact]
public void Test_MapDtoToEntity()
{
    MapperConfiguration config = new (cfg => 
        cfg.CreateMap<UserDto, UserEntity>());
    
    IMapper mapper = new Mapper(config);

    UserDto dto = new UserDto("username", "[email protected]");
    
    UserEntity entity = mapper.Map<UserEntity>(dto);
    
    Assert.Equal(dto.Email, entity.Email);
    Assert.Equal(dto.UserName, entity.UserName);

}

En este ejemplo, estamos comprobando que tanto el Email como el UserName se pasan de uno a otro correctamente.

 

 

Si esto fuera una aplicación real, lo que haríamos es instanciar todas los mapeos al inicio e incluirlos en el contenedor de dependencias e inyectarlo a través de IMapper.

 

Como has podido ver con CreateMap, creamos un mapeo, pero con CreateMap<T, U>().ReverseMap(), creamos el inverso. Obviamente podríamos utilizar otro CreateMap, no habría ningún problema, pero por norma general en las apps se hace con ReverseMap, ya que es menos código.

[Fact]
public void Test_MapDtoToEntity_Reverse()
{
    MapperConfiguration config = new (cfg => 
        cfg.CreateMap<UserDto, UserEntity>().ReverseMap());
    
    IMapper mapper = new Mapper(config);

    UserEntity entity = new UserEntity("username", "[email protected]");
    
    UserDto dto = mapper.Map<UserDto>(entity);
    
    Assert.Equal(entity.Email, dto.Email);
    Assert.Equal(entity.UserName, dto.UserName);
}

Por supuesto, este es el caso mas sencillo, pero podemos incluir reglas, por ejemplo tenemos otros casos, donde por ejemplo una propiedad no sea igual, en vez de usuario y email, tenemos un usuario con detalles de contacto:

public class UserDetailsDto
{
    public string UserName { get; set; }
    public ContactDetails ContactDetails { get; set; }
}

public class ContactDetails
{
    public string Email { get; set; }
    public string PhoneNumber { get; set; }
}

public class UserDetailsEntity
{
    public string UserName { get; set; }
    public string Email { get; set; }
}

Como puedes ver hemos pasado de usar records a usar clases con getters y setters, y esto es necesario por la “magia” que hace por detrás (que ahora explicaremos) pero, dejar los getters y setters abiertos no es la mejor idea, ya que el código será mutable, y lo mejor, en mi opinión es tener código inmutable

 

Pero bueno, sigamos, en ese caso, para mapear un objeto complejo, tenemos que incluir una regla, porque ese mapeo por razones obvias no va a suceder de manera automática, para poner una regla tenemos que utilizar `ForMember`.

[Fact]
public void Test_MapComplexObject()
{
    MapperConfiguration config = new(cfg =>
        cfg.CreateMap<UserDetailsDto, UserDetailsEntity>()
            .ForMember(destination => destination.Email,
                options => options
                        .MapFrom(source => source.ContactDetails.Email)));

    IMapper mapper = new Mapper(config);

    UserDetailsDto dto = new UserDetailsDto()
    {
        ContactDetails = new ContactDetails()
        {
            Email = "[email protected]",
            PhoneNumber = "1256555"
        },
        UserName = "username"
    };

    UserDetailsEntity entity = mapper.Map<UserDetailsEntity>(dto);

    Assert.Equal(dto.ContactDetails.Email, entity.Email);
    Assert.Equal(dto.UserName, entity.UserName);
}

 

 

2.2 - La magia de automapper, como funciona por detrás? 

Si eres nuevo programando en C# seguramente te estés preguntando cómo funciona esto, ya que parece brujería, te permite programar algo que de primeras es largo y tedioso en segundos y parece que va bien.

La respuesta es que Automapper utiliza lo que se llama Reflection, que nos permite leer los metadatos de los objetos y dinámicamente podemos ver como son, extraer la información, asignar valores, etc.

 

De primeras parece muy bonito, pero la realidad es que hacer reflection, especialmente con objetos grandes no es muy recomendable, ya que es muy lento y muy costoso; si donde lo vas a usar, tienes unas pocas llamadas, no vas a notar ningún problema, por el contrario, si la aplicación que usa el Automapper tiene miles de llamadas por minuto, el rendimiento va a empeorar drásticamente.

 

 

3 - Introducción a Mapperly en c#

La segunda librería que quiero presentar hoy es Mapperly, la cual, no es muy conocida porque es bastante nueva, además solo funciona en versiones de net 7 o superiores, esto es debido a que utiliza Source Generators.

 

Para el que no lo sepa source generators en c# es código que crea otro código en tiempo de compilación, lo que implica, que a diferencia del caso anterior donde utilizamos reflection, en este caso, vamos a utilizar código C# “normal”. 

Y si tenemos un error en el mapeo, este nos va a saltar en tiempo de compilación, no en tiempo de ejecución. 

 

3.1 - Ejemplos de Mapperly con C#

Como caso de uso tenemos exactamente el mismo que en el caso anterior, vamos a mapperar 1 a 1 un objeto, en este caso la configuración es un poco diferente. 

Tenemos que crear una partial class con el mapper e indicar el atributo Mapper, así el source generator sabe que ese código lo tiene que convertir internamente en el mapper de código. 

[Mapper]
public partial class UserMapper
{
    public partial UserEntity ToEntity(UserDto dto);
}

Y después debemos instanciar el mapper y llamar al método que hemos creado:

[Fact]
public void Test_MapDtoToEntity()
{
    UserMapper mapper = new UserMapper();
    UserDto dto = new UserDto("username", "[email protected]");

    UserEntity entity = mapper.ToEntity(dto);

    Assert.Equal(dto.Email, entity.Email);
    Assert.Equal(dto.UserName, entity.UserName);
}

Para hacer el reverso lo mismo, en ese mismo mapper, un método que convierta a DTO y luego en el test, simplemente llamamos al método:

public partial class UserMapper
{
    public partial UserEntity ToEntity(UserDto dto);
    public partial UserDto ToDto(UserEntity entity);
}

[Fact]
public void Test_MapEntityToDto()
{
    UserMapper mapper = new UserMapper();
    UserEntity entity = new UserEntity("username", "[email protected]");

    UserDto dto = mapper.ToDto(entity);

    Assert.Equal(entity.Email, dto.Email);
    Assert.Equal(entity.UserName, dto.UserName);
}

Ahora vamos a pasar a los objetos complejos como hemos visto antes, en este caso también tenemos que configurar el mapeo necesario aunque es un poco diferente: 

[Mapper]
public partial class UserDetailsMapper
{   
    [MapProperty(nameof(@UserDetailsDto.ContactDetails.Email), nameof(@UserEntity.Email))]
    public partial UserEntity ToEntity(UserDetailsDto dto);
}

[Fact]
public void Test_MapComplexDtoToEntity()
{
    UserDetailsMapper mapper = new UserDetailsMapper();
    UserDetailsDto dto = new UserDetailsDto()
    {
        ContactDetails = new ContactDetails()
        {
            Email = "[email protected]",
            PhoneNumber = "1256555"
        },
        UserName = "username"
    };

    UserEntity entity = mapper.ToEntity(dto);

    Assert.Equal(dto.ContactDetails.Email, entity.Email);
    Assert.Equal(dto.UserName, entity.UserName);
}

Como vemos también se hace con un atributo, donde especificamos el “desde donde” a “destino”. Además el símbolo @ lo que hace es permitir al sourceGenerator coger todo el namespace, si no lo ponemos, tenemos que poner el namespace de la clase completo como si fuera texto; con el arroba es mucho más fácil de ver y de mantener. 

Aunque bueno, si tienes muchos mapeos va a ser muy difícil de mantener.

 

Esto es solo una introducción, si quieres más información como por ejemplo cómo utilizar  static mappers con extension methods te recomiendo que leas su documentación. 

 

 

4 - Mi conclusión personal

Personalmente yo no soy un fan de los mappers, desde Mapperly lo soy un poco más, pero, mi rechazo viene por un trabajo que tenía donde había objetos muy grandes y las configuraciones eran eternas y muy difíciles de mantener, donde se perdía una gran cantidad de tiempo. 

Mapperly hace tanto los mapeos, como la forma de configurar los mappers algo más sencillo que automapper, además, soluciona todo el problema del rendimiento que tenía automapper (además funciona con records). 

 

Pero en ambos casos, si el objeto es grande y tiene muchas configuraciones se va a hacer muy difícil de mantener. En los últimos meses, la “regla” que he llevado es, si tiene 3 o menos configuraciones, puedo usar Mapperly, si tiene más de 3, tengo que crear el mapeo yo, y la verdad es que con esta regla estoy contento. 

 

Para terminar diré, que, el problema principal es que la gente no testea dicho mapeo, ya sea escrito por él, por Automapper o por Mapperly, prácticamente NADIE escribe test contra el mapper, así que por ahí llegan un montón de dolores de cabeza. 

 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café