Start Debugging

AutoMapper vs Mapperly vs mapeamento escrito à mão em 2026

Mapperly é o padrão para código .NET novo: iguala a velocidade do mapeamento escrito à mão, sobrevive ao Native AOT e detecta membros não mapeados em tempo de compilação. AutoMapper ainda vence no ProjectTo. Com benchmarks e limites de licença.

Para código .NET novo em 2026, use Mapperly. Ele gera C# puro em tempo de compilação, roda dentro de 3% do mapeamento escrito à mão, publica limpo sob Native AOT e transforma uma propriedade esquecida em um diagnóstico do compilador em vez de uma string vazia silenciosa. Escreva o mapeamento à mão quando um projeto tem menos de cerca de vinte mapas ou quando os formatos de origem e destino realmente divergem. Fique com o AutoMapper apenas quando ProjectTo for essencial em uma base de código grande com EF Core e você se qualificar para o nível Community gratuito, porque acima de 5.000.000 USD de receita anual a licença transforma a decisão em uma ordem de compra.

Todos os números abaixo foram medidos em um Apple M4 (10 núcleos) com .NET SDK 10.0.302 mirando net10.0, usando AutoMapper 16.2.0 (lançado em 2026-07-02), Riok.Mapperly 4.3.1 (lançado em 2025-12-22) e BenchmarkDotNet 0.15.8.

A matriz

AutoMapper 16.2.0Mapperly 4.3.1Escrito à mão
Licençacopyleft RPL-1.5 ou comercial pagaApache 2.0nenhuma
Custo acima de 5.000.000 USD de receitade 799 a 6.399 USD por anográtisgrátis
Como o mapeamento é produzidoreflexão mais árvores de expressão compiladas no primeiro usogerador de código-fonte do Roslyn em tempo de compilaçãovocê
Membro de destino não mapeadosilencioso, só AssertConfigurationIsValid() detectaaviso RMG012, escalável para erroo compilador também não diz nada
Membro de origem não mapeadonão é reportadoaviso RMG020não é reportado
Publicação com Native AOTIL2104 mais IL3053, quebra na inicializaçãozero avisos, funcionazero avisos, funciona
Custo a frio do primeiro mapeamento~33 ms para 3 mapas~1 ms0
Mapeamento de um objeto105.79 ns60.44 ns58.48 ns
Projeção com EF CoreProjectTo com expansão explícita, parâmetros e profundidade de recursãoprojeção IQueryable gerada, vários recursos não suportadosescreva o Select
Map(object, type) em runtimesimnãonão
Saída depurávelárvore de expressão compilada.g.cs legível no qual você pode entrar passo a passoseu próprio código

A licença é o eixo do qual tudo o mais depende

Em 2025-07-02 Jimmy Bogard transferiu AutoMapper e MediatR para a Lucky Penny Software e relicenciou ambos. AutoMapper 15.0.0 e posteriores são distribuídos sob um modelo duplo: a Reciprocal Public License 1.5 para uso open source, ou uma licença comercial paga. A versão 14.x e anteriores permanecem sob MIT para sempre.

RPL-1.5 não é MIT com passos extras. É um copyleft recíproco forte que alcança software implantado, não apenas software distribuído, então produtos comerciais de código fechado não podem realmente ser publicados sobre o build RPL. Isso deixa o acordo comercial, cujo nível Community gratuito cobre organizações com menos de 5.000.000 USD de receita bruta anual que também tenham recebido menos de 10.000.000 USD de capital externo, e que não sejam entidades governamentais, quase governamentais ou de ensino superior. Acima dessa linha, os níveis publicados são Standard a 799 USD por ano para 1 a 10 desenvolvedores, Professional a 1.499 USD por ano para 11 a 50, e Enterprise a 6.399 USD por ano para desenvolvedores ilimitados. Contam apenas os desenvolvedores que escrevem ou mantêm ativamente código que chama a biblioteca, o que exclui QA, design e trabalho de front-end.

A aplicação da licença é deliberadamente branda. Não há servidor de licenças, nem chamada de rede, nem bloqueio de recursos. Uma chave ausente ou expirada produz uma mensagem de log e nada mais, e desde a 16.2.0 a chave também pode vir das variáveis de ambiente AUTOMAPPER_LICENSE_KEY ou LUCKYPENNY_LICENSE_KEY em vez de cfg.LicenseKey. Mas aplicação branda não é o mesmo que permissão, e “não notamos um aviso nos logs” não é uma posição de licenciamento que alguém queira defender em uma revisão de compras.

É a mesma bifurcação das bibliotecas de mediator, e o raciocínio se transfere diretamente: veja MediatR vs classes de serviço simples em 2026 para o detalhamento completo do nível Community e das obrigações da RPL-1.5.

Quando escolher Mapperly

Quando escolher o mapeamento escrito à mão

Quando ficar com o AutoMapper

Se você decidir sair, a mecânica está coberta passo a passo em migrar do AutoMapper para mapeamento gerado com Mapperly.

O benchmark

O modelo é um Order com cinco membros escalares, um Customer aninhado, cinco filhos OrderLine e um enum mapeado para o nome em texto. [MemoryDiagnoser], job padrão, e a compilação de expressões do AutoMapper aquecida no [GlobalSetup] para que a medição seja vazão em estado estável e não o custo da primeira chamada.

// .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 em um Apple M4, 10 núcleos físicos, .NET 10.0.10 Arm64 RyuJIT:

MétodoMédiaRatioAlocadoRatio de alocação
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

Leia isso com honestidade: 45 nanossegundos por objeto não é o motivo pelo qual você deveria mudar. Em uma requisição que mapeia 1.000 pedidos toda a diferença são 31 microssegundos, que não vão aparecer ao lado de uma única ida ao banco de dados. O argumento de desempenho só pesa de verdade com contagens de objetos muito altas, e é o mais fraco dos três motivos para preferir Mapperly.

A diferença de 40.000 bytes entre Mapperly e o mapeamento escrito à mão no caso de 1.000 objetos é um artefato real que vale a pena entender. O Mapperly alarga o parâmetro de um mapeador de coleção aninhada gerado para 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 uma List<T> através de uma interface faz boxing do enumerador struct: 40 bytes por pedido, 40.000 bytes no lote inteiro. Declarar você mesmo o mapeador da coleção aninhada com um parâmetro concreto List<OrderLine> elimina isso. Esse é exatamente o tipo de coisa que você consegue encontrar e corrigir porque o código gerado está em disco, que é a diferença prática entre um gerador de código-fonte e uma árvore de expressão compilada.

O detalhe que decide por você: Native AOT

Publique uma aplicação de console que chama o AutoMapper 16.2.0 com <PublishAot>true</PublishAot> em net10.0 e o build avisa:

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

Avisos são fáceis de ignorar. O binário resultante não é:

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)

O trimmer removeu um método que o ExpressionBuilder procura por reflexão, então o construtor estático morre antes do seu primeiro mapeamento. A aplicação equivalente com Mapperly publicada com as mesmas configurações emite zero avisos IL, produz um binário nativo de 1.1 MB e funciona. Isso não é um problema de ajuste que você resolva com atributos DynamicDependency no ponto da chamada; é uma propriedade de construir mapas a partir de árvores de expressão em runtime, que é a mesma armadilha descrita em o que é código trim-safe e como escrevê-lo. Se Native AOT está no seu roteiro, a decisão já está tomada.

A versão mais branda do mesmo efeito é a inicialização a frio. Construir a configuração e executar o primeiro mapeamento para três tipos levou 33 milissegundos nesta máquina, contra 1 milissegundo para new OrderMapper() mais a primeira chamada. Em uma aplicação web de vida longa isso é invisível. Em uma Lambda é uma fatia mensurável de uma invocação a frio, que é por isso que aparece em reduzir o tempo de inicialização a frio de uma Lambda AWS com .NET.

Onde a diferença de segurança realmente aparece

Adicione uma propriedade Slug a um DTO e esqueça de mapeá-la. O AutoMapper 16.2.0 mapeia o objeto mesmo assim:

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

AssertConfigurationIsValid() detecta sim, lançando AutoMapperConfigurationException com “Unmapped members were found”, mas só se você lembrou de chamá-lo, e só para membros de destino não mapeados. Uma propriedade de origem que não chega mais a nenhum DTO não é reportada de jeito nenhum.

O Mapperly reporta as duas direções em tempo de compilação, com o texto real da mensagem:

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

Por padrão são avisos, o que significa que vão se afogar em um build barulhento. Escale-os no .editorconfig e o build falha de vez:

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

Essa é a configuração que transforma o Mapperly de “um AutoMapper mais rápido” em uma categoria diferente de ferramenta: bugs de mapeamento deixam de ser incidentes em produção e viram falhas de build. É também a ilustração mais clara de por que os geradores de código-fonte valem a dependência em tempo de compilação.

O mapeamento escrito à mão, que fique registrado, não oferece essa verificação. Uma atribuição esquecida em um método ToDto é exatamente tão silenciosa quanto no AutoMapper. A segurança dele vem de ser visível na revisão de código, não de ferramentas.

A decisão

Use Mapperly por padrão em código novo, e escale RMG012 e RMG020 para erros no primeiro dia para de fato colher o benefício. Escreva o mapeamento à mão quando o projeto é pequeno ou os formatos são irregulares, e aceite que você está trocando verificações de ferramentas por revisabilidade. Fique com o AutoMapper quando uma base de código madura e carregada de ProjectTo já funciona, você está abaixo do limite Community e Native AOT não está no roteiro; e se qualquer uma dessas três coisas deixar de ser verdade, comece a migração em vez de orçar a licença. A tabela de desempenho é a parte menos interessante desta comparação. Segurança sob trimming e diagnósticos em tempo de compilação são o que realmente muda como uma base de código se comporta.

Relacionado

Fontes

Comments

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

< Voltar