El rendimiento de los mappers en C#

Esta pasada semana publiqué un vídeo sobre auto mappers en C#, pese a durar 20 minutos parece que no cubrí todo lo que se esperaba, varias personas me preguntaron por comparaciones de otros mappers u otras funcionalidades, pues aquí vamos a verlo todo, y además comprobaremos los benchmarks en cada caso. 

 

 

1 - Qué es un benchmark? 

Lo primero que quiero explicar es qué es un benchmark, y pese a ya tener un post al detalle sobre los benchmarks en c#, simplemente un pequeño repaso, que nunca viene mal. 

Un benchmark es una comparativa de rendimiento, aquí podemos entender por rendimiento varios elementos como pueden ser, la memoria que utiliza, o el tiempo que tarda. 

 

En .NET hacemos benchmarks con la librería benchmarkdotnet (enlace) la cual es muy completa y permite incluir información al detalle, por ejemplo si solo quieres saber cuánta memoria está siendo alocada, puedes comprobar solo la memoria, si quieres saber cuánto tiempo tarda, puedes comprobar solo el tiempo, etc. 

Para nuestro ejemplo, comprobaremos memoria y tiempo. 

 

Para utilizar la librería, tienes que utilizar el atributo [Benchmark] sobre el método o métodos que quieres comprobar el rendimiento. Si quieres comprobar varios, estos métodos, tienen que estar en la misma clase, así que es normal hacer una clase benchmark donde llamas a todo:

[MemoryDiagnoser]
[KeepBenchmarkFiles(false)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByMethod)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class MapperBenchmark
{
    private readonly AccountInformationDto _accountDto = MapperHelper.BuildDefaultDto();
    private readonly AutomapperExample _automapperExample = new AutomapperExample();
    private readonly ManualMapperExample _manualMapperExample = new ManualMapperExample();
    private readonly MapperlyExample _mapperlyExample = new MapperlyExample();
    private readonly MapsterExample _mapsterExample = new MapsterExample();

    [Benchmark]
    public void Automapper()
    {
        _ = _automapperExample.Execute(_accountDto);
    }

    [Benchmark]
    public void Manual()
    {
        _ = _manualMapperExample.Execute(_accountDto);
    }

    [Benchmark]
    public void Mapperly()
    {
        _ = _mapperlyExample.Execute(_accountDto);
    }

    [Benchmark]
    public void Mapster()
    {
        _ = _mapsterExample.Execute(_accountDto);
    }
}

Y como he dicho antes, tenemos esta clase y luego en nuestra aplicación debemos de llamar al benchmark runner, pasado como tipo la clase en la que están los benchmarks.

BenchmarkRunner.Run<MapperBenchmark>();

Otra cosa a tener en cuenta es que cuando ejecutes la aplicación para correr los benchmarks tienes que hacerlo en modo release, ya que en modo debug no sería realista de cara a producción.

 

 

2 - Comparar los diferentes mappers de .NET

He comentado hace un momento que este post viene a raíz de los comentarios del vídeo sobre los mappers, así que no voy a entrar en detalle sobre cómo funciona cada uno otra vez. Lo que sí que voy a hacer es comentar el ejemplo.

Para nuestro ejemplo vamos a utilizar el mapeo entre un DTO y un Entity, el DTO es simple, pero contiene una lista la cual es un tipo que tiene otro objeto dentro, simplemente porque había gente en el video anterior que me pregunto por esto, esta es la estructura: 

public class AccountInformationDto
{
    public int AccountId { get; set; }
    public List<UserDetailsDto> Users { get; set; }
}

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

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

Y luego la entidad es muy similar, tiene un cambio y es que en nuestro UserEntity no tenemos información de contacto, sino que el email va directo en el usuario, esto quiere decir que tenemos que hacer un “flatten” que se llama en inglés. Y lo que es más importante, la configuración por defecto de los mappers no nos va a funcionar para ese objeto, así que veremos dicha configuración en este post. 

public class AccountInformationDetailsEntity
{
    public int AccountId { get; set; }
    public List<UserDetailsEntity> Users { get; set; }
}

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

Pese a ello, sigue siendo un objeto pequeño, así que se mapeará rápido.

Pero bueno vamos al lío

 

 

2.1- Mapear listas con automapper

Ya vimos en el post anterior cómo podemos mapear objetos, en este caso, vamos a hacerlo con una lista, a la configuración que teníamos en el post anterior, le añadimos la del objeto padre y ya esta. 

public class AutomapperExample
{
    private readonly IMapper _mapper;

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

            cfg.CreateMap<AccountInformationDto, AccountInformationDetailsEntity>()
                .ForMember(destination => destination.Users,
                    options => options.MapFrom(a => a.Users));
        });

        _mapper = new Mapper(config);
    }
    
    public AccountInformationDetailsEntity Execute(AccountInformationDto accountInfoDto)
        => _mapper.Map<AccountInformationDetailsEntity>(accountInfoDto);
}

Esto nos indica que tenemos que mapear básicamente cada objeto que necesitamos, y con un mapeo automático a no ser que tengan nombres diferentes es suficiente.

Y luego llamamos al mapeo igual que lo hicimos en el otro post, _mapper.Map<T>(obj).

Nota: Con AutoMapper podemos crear lo que se llaman profiles, que es una forma de ubicar toda la configuración en un mismo sitio,  lo cual está bastante bien.

 

2.2 - Mapear listas con mapperly

La segunda librería que vimos fue mapperly, y vimos un ejemplo normal, ahora vamos a crear un escenario con el objeto que he explicado antes. 

Ya sabemos que tenemos que crear una clase parcial para el mapeo, pues hacemos lo mismo, e incluimos la clase padre, luego, para el flatten tenemos que incluir la configuración para indicar de donde a donde va a ir, y ya está:

[Mapper]
public partial class AccountInformationMapper
{   
    public partial AccountInformationDetailsEntity ToEntityMapperly(AccountInformationDto dto);
    
    [MapProperty(nameof(@UserDetailsDto.ContactDetails.Email), nameof(@UserDetailsEntity.Email))]
    public partial UserDetailsEntity ToEntity(UserDetailsDto dto);
}

public class MapperlyExample
{
    private readonly AccountInformationMapper _mapper = new();
    
    public AccountInformationDetailsEntity Execute(AccountInformationDto accountInfoDto)
        => _mapper.ToEntityMapperly(accountInfoDto);
}

Mapperly usa por detrás code generations, y es suficientemente inteligente para saber que tiene que usar el método que hemos creado cuando mapea la lista de usuarios.

 

 

2.3 - Mapear listas con mapster

En el post original, este mapper no lo incluí, y el motivo por el que no lo incluí fue que no lo he usado nunca y ya sabéis que yo suelo explicar, salvo en excepciones como está, librerías, patrones, etc, con los que he trabajado y tengo experiencia; Así que para mi, esta implementación es nueva.

Igual que en cualquier otro mapper, si los nombres de las propiedades de ambos objetos es el mismo, no tienes que poner configuración adicional.

 

Pero si que lo tienes que hacer si el nombre es diferente, como en nuestro caso, donde estamos haciendo un flat de uno de los objetos: 

 

public class MapsterExample
{
    public MapsterExample()
    {
        TypeAdapterConfig<UserDetailsDto, UserDetailsEntity>
            .NewConfig()
            .Map(dest => dest.Email, src => src.ContactDetails.Email);
    }

    public AccountInformationDetailsEntity Execute(AccountInformationDto accountInfoDto)
        => accountInfoDto.Adapt<AccountInformationDetailsEntity>();
}

Y para llamar al objeto, simplemente utilizamos el método Adapt.

 

Una cosa que he notado es que la configuración se hace con TypeAdapterConfig, lo que  quiere decir que se puede hacer en cualquier lugar de la aplicación y afectará a todos los usos. Pero como primer acercamiento, me ha parecido muy simple de entender.

 

 

2.4 - Mapear listas de forma manual

Cuando mapeas de forma manual hay varias formas de hacerlo, la que me gusta a mi es usar una clase estática, con un método estático llamado .ToXXXX donde XXXX es en lo que vas a convertir, en mi caso, ToEntity:

public class ManualMapperExample
{
    public AccountInformationDetailsEntity Execute(AccountInformationDto accountInfoDto)
        => accountInfoDto.ToEntity();
}

public static class UserManualMapper
{
    public static AccountInformationDetailsEntity ToEntity(this AccountInformationDto dto)
    {
        return new AccountInformationDetailsEntity()
        {
            AccountId = dto.AccountId,
            Users = dto.Users.Select(ToEntity).ToList()
        };
    }

    public static UserDetailsEntity ToEntity(this UserDetailsDto dto)
        => new UserDetailsEntity()
        {
            UserName = dto.UserName,
            Email = dto.ContactDetails.Email
        };
}

 

 

3 - El momento de la verdad - Qué mapper es el mejor? 

Antes de pasar a los resultados quiero hacer un pequeño parón para decir lo siguiente: Da igual como crees el mapeo, sí con una librería o de forma manual, pero, lo que SIEMPRE tienes que hacer es tests que cubren dicho mapeo.

 

Non tienen porque ser test unitarios, simplemente puedes hacer uno de integración y testear cada una de las salidas si quieres, pero claro, más fácil uno unitario para esto, pero bueno da igual, lo que me refiero es que tienes que testear cada una de las asignaciones, porque sino, luego llegan los problemas. 

 

Y bueno, estos son los resultados para un objeto AccountInformationDto con 100k usuarios

Benchmarking automappers

  • Para la tabla, la segunda columna es el tiempo que tarda y la última la memoria alocada, en toda la tabla, cuanto menos, mejor.

 

La verdad es que viendo los resultados no me los esperaba para nada. Bueno, Automapper si me esperaba que fuera el más lento, pero luego, me esperaba que el mapeo manual fuera más rápido que los otros dos. 

 

Si que es verdad, que la diferencia es mínima, aún así, no deja de sorprenderme.

Pese a esto, yo, personalmente prefiero utilizar un mapeo manual, osea construir yo mismo el mapeo, ya que en mi opinion es mas facil de leer y si tenemos configuración algo mas compleja, entender, aunque claro, con estos datos no puedo discutirle, más allá de ser más o menos legibles, nada a alguien que quiera utilizar Mapster o Mapperly. 

 

 


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é