Start Debugging

AutoMapper vs Mapperly vs mapeo escrito a mano en 2026

Mapperly es la opción por defecto para código .NET nuevo: iguala la velocidad del mapeo escrito a mano, sobrevive a Native AOT y detecta miembros sin mapear en tiempo de compilación. AutoMapper sigue ganando en ProjectTo. Con benchmarks y umbrales de licencia.

Para código .NET nuevo en 2026, usa Mapperly. Genera C# plano en tiempo de compilación, corre dentro de un 3% del mapeo escrito a mano, se publica limpio bajo Native AOT y convierte una propiedad olvidada en un diagnóstico del compilador en lugar de una cadena vacía silenciosa. Escribe el mapeo a mano cuando un proyecto tiene menos de unos veinte mapas o cuando las formas de origen y destino realmente divergen. Quédate con AutoMapper solo cuando ProjectTo sea crítico en una base de código grande con EF Core y califiques para el nivel Community gratuito, porque por encima de 5 000 000 USD de ingresos anuales la licencia convierte la decisión en una orden de compra.

Todos los números de abajo se midieron en un Apple M4 (10 núcleos) con .NET SDK 10.0.302 apuntando a net10.0, usando AutoMapper 16.2.0 (publicado el 2026-07-02), Riok.Mapperly 4.3.1 (publicado el 2025-12-22) y BenchmarkDotNet 0.15.8.

La matriz

AutoMapper 16.2.0Mapperly 4.3.1Escrito a mano
Licenciacopyleft RPL-1.5 o comercial de pagoApache 2.0ninguna
Costo por encima de 5 000 000 USD de ingresosde 799 a 6 399 USD al añogratisgratis
Cómo se produce el mapeoreflexión más árboles de expresión compilados en el primer usogenerador de código fuente de Roslyn en tiempo de compilación
Miembro de destino sin mapearsilencioso, solo lo detecta AssertConfigurationIsValid()advertencia RMG012, escalable a errorel compilador tampoco te dice nada
Miembro de origen sin mapearno se reportaadvertencia RMG020no se reporta
Publicación con Native AOTIL2104 más IL3053, falla al arrancarcero advertencias, funcionacero advertencias, funciona
Costo en frío del primer mapeo~33 ms para 3 mapas~1 ms0
Mapeo de un objeto105.79 ns60.44 ns58.48 ns
Proyección con EF CoreProjectTo con expansión explícita, parámetros y profundidad de recursiónproyección IQueryable generada, varias características no soportadasescribe el Select
Map(object, type) en runtimenono
Salida depurableárbol de expresión compilado.g.cs legible en el que puedes entrar paso a pasotu propio código

La licencia es el eje del que cuelga todo lo demás

El 2025-07-02 Jimmy Bogard trasladó AutoMapper y MediatR a Lucky Penny Software y relicenció ambos. AutoMapper 15.0.0 y posteriores se distribuyen bajo un modelo dual: la Reciprocal Public License 1.5 para uso open source, o una licencia comercial de pago. La versión 14.x y anteriores permanecen bajo MIT para siempre.

RPL-1.5 no es MIT con pasos extra. Es un copyleft recíproco fuerte que alcanza al software desplegado, no solo al distribuido, así que los productos comerciales de código cerrado no pueden realmente publicarse sobre la compilación RPL. Eso deja el acuerdo comercial, cuyo nivel Community gratuito cubre a organizaciones con menos de 5 000 000 USD de ingresos brutos anuales que además hayan recibido menos de 10 000 000 USD de capital externo, y que no sean entidades gubernamentales, cuasi gubernamentales o de educación superior. Por encima de esa línea, los niveles publicados son Standard a 799 USD al año para 1 a 10 desarrolladores, Professional a 1 499 USD al año para 11 a 50, y Enterprise a 6 399 USD al año para desarrolladores ilimitados. Solo cuentan los desarrolladores que escriben o mantienen activamente código que llama a la biblioteca, lo que excluye QA, diseño y trabajo de front-end.

La aplicación de la licencia es deliberadamente suave. No hay servidor de licencias, ni llamada de red, ni bloqueo de características. Una clave ausente o vencida produce un mensaje de log y nada más, y desde 16.2.0 la clave también puede venir de las variables de entorno AUTOMAPPER_LICENSE_KEY o LUCKYPENNY_LICENSE_KEY en lugar de cfg.LicenseKey. Pero aplicación suave no es lo mismo que permiso, y “no notamos una advertencia en los logs” no es una postura de licencia que nadie quiera defender en una revisión de compras.

Es la misma bifurcación que con las bibliotecas de mediator, y el razonamiento se traslada directamente: revisa MediatR vs clases de servicio simples en 2026 para el desglose completo del nivel Community y las obligaciones de RPL-1.5.

Cuándo elegir Mapperly

Cuándo elegir el mapeo escrito a mano

Cuándo quedarse con AutoMapper

Si decides irte, la mecánica está cubierta paso a paso en migrar de AutoMapper a mapeo generado con Mapperly.

El benchmark

El modelo es un Order con cinco miembros escalares, un Customer anidado, cinco hijos OrderLine y un enum mapeado a su nombre en texto. [MemoryDiagnoser], job por defecto, y la compilación de expresiones de AutoMapper precalentada en [GlobalSetup] para que la medición sea rendimiento en estado estable y no el costo de la primera llamada.

// .NET SDK 10.0.302, net10.0, C# 14
// AutoMapper 16.2.0, Riok.Mapperly 4.3.1, BenchmarkDotNet 0.15.8
[MemoryDiagnoser]
public class MappingBenchmarks
{
    private Order _order = null!;
    private List<Order> _orders = null!;
    private IMapper _autoMapper = null!;
    private OrderMapper _mapperly = null!;

    [GlobalSetup]
    public void Setup()
    {
        _order = MakeOrder(1);
        _orders = Enumerable.Range(1, 1000).Select(MakeOrder).ToList();

        var config = new MapperConfiguration(
            cfg => cfg.AddProfile<OrderProfile>(),
            NullLoggerFactory.Instance);
        _autoMapper = config.CreateMapper();
        _mapperly = new OrderMapper();

        _autoMapper.Map<OrderDto>(_order); // warm the expression compilation
    }

    [Benchmark(Baseline = true)]
    public OrderDto HandWritten_Single() => HandMapper.ToDto(_order);

    [Benchmark]
    public OrderDto Mapperly_Single() => _mapperly.ToDto(_order);

    [Benchmark]
    public OrderDto AutoMapper_Single() => _autoMapper.Map<OrderDto>(_order);
}

Resultados en un Apple M4, 10 núcleos físicos, .NET 10.0.10 Arm64 RyuJIT:

MétodoMediaRatioAsignadoRatio de asignación
HandWritten_Single58.48 ns1.00624 B1.00
Mapperly_Single60.44 ns1.03624 B1.00
AutoMapper_Single105.79 ns1.81704 B1.13
HandWritten_100072,696 ns1.00632,091 B1.00
Mapperly_100077,334 ns1.06672,093 B1.06
AutoMapper_1000103,376 ns1.42720,640 B1.14

Léelo con honestidad: 45 nanosegundos por objeto no es la razón por la que deberías cambiar. En una petición que mapea 1 000 pedidos toda la diferencia son 31 microsegundos, que no van a aparecer al lado de un solo viaje a la base de datos. El argumento de rendimiento solo pesa de verdad con conteos de objetos muy altos, y es la más débil de las tres razones para preferir Mapperly.

La brecha de 40 000 bytes entre Mapperly y el mapeo escrito a mano en el caso de 1 000 objetos es un artefacto real que vale la pena entender. Mapperly ensancha el parámetro de un mapeador de colección anidada generado a IReadOnlyCollection<T>:

// Riok.Mapperly 4.3.1 generated output, trimmed
private List<OrderLineDto> MapToListOfOrderLineDto(IReadOnlyCollection<OrderLine> source)
{
    var target = new List<OrderLineDto>(source.Count);
    foreach (var item in source)
        target.Add(MapToOrderLineDto(item));
    return target;
}

Enumerar una List<T> a través de una interfaz aplica boxing a su enumerador de struct: 40 bytes por pedido, 40 000 bytes en todo el lote. Declarar tú mismo el mapeador de la colección anidada con un parámetro concreto List<OrderLine> lo elimina. Este es exactamente el tipo de cosa que puedes encontrar y arreglar porque el código generado está en disco, que es la diferencia práctica entre un generador de código fuente y un árbol de expresión compilado.

El detalle que decide por ti: Native AOT

Publica una aplicación de consola que llama a AutoMapper 16.2.0 con <PublishAot>true</PublishAot> en net10.0 y la compilación advierte:

AutoMapper.dll : warning IL2104: Assembly 'AutoMapper' produced trim warnings.
AutoMapper.dll : warning IL3053: Assembly 'AutoMapper' produced AOT analysis warnings.

Las advertencias son fáciles de ignorar. El binario resultante no lo es:

Unhandled exception. System.TypeInitializationException: A type initializer threw an exception.
 ---> System.ArgumentNullException: Value cannot be null. (Parameter 'method')
   at System.Linq.Expressions.Expression.Call(MethodInfo, Expression)
   at AutoMapper.Execution.ExpressionBuilder..cctor()
   at AutoMapper.MapperConfiguration..ctor(MapperConfigurationExpression, ILoggerFactory)

El trimmer eliminó un método que ExpressionBuilder busca por reflexión, así que el constructor estático muere antes de tu primer mapeo. La aplicación equivalente con Mapperly publicada con la misma configuración emite cero advertencias IL, produce un binario nativo de 1.1 MB y funciona. Eso no es un problema de ajuste que puedas resolver con atributos DynamicDependency en el sitio de la llamada; es una propiedad de construir mapas a partir de árboles de expresión en runtime, que es la misma trampa descrita en qué es el código trim-safe y cómo lo escribo. Si Native AOT está en tu hoja de ruta, la decisión ya está tomada.

La versión suave del mismo efecto es el arranque en frío. Construir la configuración y ejecutar el primer mapeo para tres tipos tomó 33 milisegundos en esta máquina, frente a 1 milisegundo para new OrderMapper() más su primera llamada. En una aplicación web de vida larga eso es invisible. En una Lambda es una porción medible de una invocación en frío, que es por lo que aparece en reducir el tiempo de arranque en frío de una Lambda de AWS con .NET.

Dónde aparece de verdad la diferencia de seguridad

Agrega una propiedad Slug a un DTO y olvida mapearla. AutoMapper 16.2.0 mapea el objeto de todos modos:

map ok: Id=1 Name=n Slug=''

AssertConfigurationIsValid() sí lo detecta, lanzando AutoMapperConfigurationException con “Unmapped members were found”, pero solo si te acordaste de llamarlo, y solo para miembros de destino sin mapear. Una propiedad de origen que ya no llega a ningún DTO no se reporta en absoluto.

Mapperly reporta ambas direcciones en tiempo de compilación, con el texto real del mensaje:

warning RMG020: The member InternalNote on the mapping source type Diag.Source
                is not mapped to any member on the mapping target type Diag.Target
warning RMG012: The member Slug on the mapping target type Diag.Target
                was not found on the mapping source type Diag.Source

Son advertencias por defecto, lo que significa que se ahogarán en una compilación ruidosa. Escálalas en .editorconfig y la compilación falla de plano:

[*.cs]
dotnet_diagnostic.RMG012.severity = error
dotnet_diagnostic.RMG020.severity = error

Ese es el ajuste que convierte a Mapperly de “un AutoMapper más rápido” en una categoría distinta de herramienta: los errores de mapeo dejan de ser incidentes en producción y pasan a ser fallos de compilación. Es también la ilustración más clara de por qué los generadores de código fuente valen la dependencia en tiempo de compilación.

El mapeo escrito a mano, para que conste, no ofrece tal verificación. Una asignación olvidada en un método ToDto es exactamente igual de silenciosa que en AutoMapper. Su seguridad viene de ser visible en la revisión de código, no de las herramientas.

La decisión

Usa Mapperly por defecto para código nuevo, y escala RMG012 y RMG020 a errores desde el primer día para obtener realmente el beneficio. Escribe el mapeo a mano cuando el proyecto es pequeño o las formas son irregulares, y acepta que estás cambiando verificaciones de herramientas por revisabilidad. Quédate con AutoMapper cuando una base de código madura y cargada de ProjectTo ya funciona, estás por debajo del umbral Community y Native AOT no está en la hoja de ruta; y si cualquiera de esas tres cosas deja de ser cierta, empieza la migración en lugar de presupuestar la licencia. La tabla de rendimiento es la parte menos interesante de esta comparación. La seguridad frente al trimming y los diagnósticos en tiempo de compilación son lo que de verdad cambia cómo se comporta una base de código.

Relacionado

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver