Start Debugging

Migrar do AutoMapper para mapeamento gerado por codigo-fonte com Mapperly

Um checklist passo a passo para substituir os Profiles, IMapper, ForMember e ProjectTo do AutoMapper 15 por mappers gerados por codigo-fonte com Riok.Mapperly 4.3 no .NET 11.

Substituir o AutoMapper por um gerador de código-fonte é uma refatoração mecânica, arquivo por arquivo, não uma reescrita. Para um serviço típico com 30-80 mapeamentos espalhados por algumas classes Profile, reserve de meio dia a um dia: cada CreateMap<Source, Dest>() vira um método partial em uma classe [Mapper], as chamadas a IMapper.Map<T> viram chamadas diretas a métodos, e ProjectTo<T>() vira uma extensão IQueryable gerada. O que quebra é tudo que dependia da flexibilidade em runtime do AutoMapper: as chamadas dinâmicas Map(object), os IValueResolver com serviços injetados, e o registro por varredura de assemblies. Vale a pena fazer se você está acima do limite de receita de US$ 5.000.000 do AutoMapper e não quer comprar uma licença, se você quer que Native AOT e trimming funcionem, ou se você quer que propriedades não mapeadas falhem a compilação em vez de serem descartadas silenciosamente em runtime.

Versões referenciadas: este guia cobre sair do AutoMapper 14.x (a última versão MIT) e 15.0 (a primeira versão com licença dupla Reciprocal Public License 1.5 / comercial da Lucky Penny Software). A substituição tem como alvo <TargetFramework>net11.0</TargetFramework> com o SDK do .NET 11, C# 14 e Riok.Mapperly 4.3.1 (lançada em 2025-12-22). Se você ainda está decidindo se vai sair, é a mesma história de fornecedor do MediatR, então leia migrar do MediatR para injeção de dependência simples para o caso paralelo; este artigo assume que a decisão já foi tomada.

Por que as equipes estão saindo do AutoMapper justamente agora

O que quebra

ÁreaMudançaSeveridade
Injeção de IMapperSubstituída pela classe mapper gerada concreta, injetada diretamentealta
Profile + CreateMap<,>()Colapsam em uma classe parcial [Mapper] com um método partial por direçãoalta
ForMember(... MapFrom ...)Substituído por [MapProperty] ou um método de mapeamento privadoalta
ReverseMap()Não há reverso automático; você declara o método da direção de retorno explicitamentemédia
IValueResolver / ITypeConverter com DISubstituídos por um construtor no mapper mais um método privadomédia
ProjectTo<T>(config)Substituído por uma extensão de projeção IQueryable<T> geradamédia
Varredura AddAutoMapper(assembly)Substituída por registro explícito AddSingleton<TMapper>()média
mapper.Map(object, type) (dinâmico)Não há ponto de entrada sem tipo em runtime; cada mapeamento é um método tipadoalta
Teste AssertConfigurationIsValid()Redundante; a compilação é a asserçãobaixa

Checklist de pré-voo

  1. Instale o SDK do .NET 11 e confirme que dotnet --version reporta 11.0.x.
  2. Inventarie seus mapeamentos. Rode um grep de CreateMap< para contar as configurações e de \.Map< e ProjectTo< para contar os pontos de chamada. Esse é o escopo da sua migração.
  3. Encontre os mapeamentos dinâmicos. Faça grep das chamadas Map( que passam um argumento Type ou uma origem object. Essas não têm equivalente direto no Mapperly e precisam de um método tipado ou um switch manual; trate delas primeiro.
  4. Encontre os resolvers. Faça grep de IValueResolver, ITypeConverter e IMappingAction. Cada um precisa de um método privado no mapper.
  5. Não precisa de backup especial, crie a branch normalmente. Esta mudança é reversível arquivo por arquivo (veja Plano de rollback), então uma branch de funcionalidade basta.

Passos da migração

1. Adicione o Mapperly junto ao AutoMapper

Instale os dois pacotes para poder migrar um profile por vez:

# .NET 11 SDK, run from the project directory
dotnet add package Riok.Mapperly --version 4.3.1

O Mapperly entrega apenas um analisador e os atributos de Riok.Mapperly.Abstractions, então não adiciona nenhuma dependência em runtime. Verifique que a compilação continua funcionando: dotnet build sem erros novos. Deixe o pacote do AutoMapper referenciado até o último profile desaparecer.

2. Converta um profile simples para uma classe [Mapper]

Pegue o profile menor primeiro. O antes:

// AutoMapper 15.0
public class CarProfile : Profile
{
    public CarProfile()
    {
        CreateMap<Car, CarDto>();
    }
}

O depois é uma classe parcial com um método parcial. O Mapperly preenche o corpo:

// .NET 11, C# 14, Riok.Mapperly 4.3.1
using Riok.Mapperly.Abstractions;

[Mapper]
public partial class CarMapper
{
    public partial CarDto ToDto(Car car);
}

Verifique: compile o projeto e abra o arquivo gerado (ele aparece na saída do analisador, ou defina <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> no .csproj para gravá-lo em disco). Confirme que cada propriedade de CarDto é atribuída. Se alguma não for, o Mapperly emite um diagnóstico como RMG020 para um membro de destino não mapeado; isso é o recurso, não uma falha.

3. Traduza ForMember para [MapProperty]

A personalização por membro do AutoMapper mapeia para atributos do Mapperly. Um renome:

// AutoMapper 15.0
CreateMap<Car, CarDto>()
    .ForMember(d => d.ModelName, o => o.MapFrom(s => s.Model));

vira um atributo [MapProperty] que nomeia os membros de origem e destino:

// .NET 11, C# 14, Riok.Mapperly 4.3.1
[MapProperty(nameof(Car.Model), nameof(CarDto.ModelName))]
public partial CarDto ToDto(Car car);

Para um valor calculado, a lambda MapFrom em linha do AutoMapper vira um método de mapeamento privado referenciado por Use:

// .NET 11, C# 14, Riok.Mapperly 4.3.1
[Mapper]
public partial class CarMapper
{
    [MapProperty(nameof(Car.Price), nameof(CarDto.Price), Use = nameof(FormatPrice))]
    public partial CarDto ToDto(Car car);

    private string FormatPrice(decimal price) => price.ToString("C");
}

Para descartar uma propriedade deliberadamente, substitua o Ignore() do AutoMapper por [MapperIgnoreTarget(nameof(CarDto.Internal))] ou [MapperIgnoreSource(nameof(Car.Secret))]. Verifique cada conversão checando que o corpo gerado atribui exatamente os membros que você espera.

4. Substitua ReverseMap() por um método explícito

O ReverseMap() do AutoMapper é uma única chamada. O Mapperly não tem reverso automático, então declare a direção de retorno como seu próprio método no mesmo mapper:

// .NET 11, C# 14, Riok.Mapperly 4.3.1
[Mapper]
public partial class CarMapper
{
    public partial CarDto ToDto(Car car);
    public partial Car ToEntity(CarDto dto);
}

É mais código, mas é honesto: o mapeamento inverso não é mais implícito, então uma propriedade assimétrica (um campo de destino sem origem) aparece como seu próprio diagnóstico em vez de ser ignorada silenciosamente. Verifique os dois corpos gerados.

5. Mova os resolvers com dependências para o construtor do mapper

Um IValueResolver do AutoMapper que precisa de um serviço injetado:

// AutoMapper 15.0
public class PriceResolver : IValueResolver<Car, CarDto, string>
{
    private readonly ICurrencyService _currency;
    public PriceResolver(ICurrencyService currency) => _currency = currency;
    public string Resolve(Car s, CarDto d, string m, ResolutionContext ctx)
        => _currency.Format(s.Price);
}

vira um construtor no mapper mais um método privado. O Mapperly gera um construtor que repassa os parâmetros que você declara, então o mapper participa da injeção de construtor normal:

// .NET 11, C# 14, Riok.Mapperly 4.3.1
[Mapper]
public partial class CarMapper
{
    private readonly ICurrencyService _currency;
    public CarMapper(ICurrencyService currency) => _currency = currency;

    [MapProperty(nameof(Car.Price), nameof(CarDto.Price), Use = nameof(FormatPrice))]
    public partial CarDto ToDto(Car car);

    private string FormatPrice(decimal price) => _currency.Format(price);
}

Verifique resolvendo o mapper a partir do contêiner em um teste e checando o preço formatado.

6. Converta ProjectTo<T> para uma projeção gerada

O ProjectTo<T>() do AutoMapper constrói uma árvore de Expression para que o EF Core possa traduzir o mapeamento em SQL. O Mapperly gera o mesmo tipo de extensão IQueryable quando você declara um método que recebe e retorna IQueryable<T>:

// .NET 11, C# 14, Riok.Mapperly 4.3.1
[Mapper]
public static partial class CarQueryMapper
{
    public static partial IQueryable<CarDto> ProjectToDto(this IQueryable<Car> q);
}

O ponto de chamada muda de .ProjectTo<CarDto>(_config) para .ProjectToDto():

// .NET 11, EF Core 11
var dtos = await db.Cars
    .Where(c => c.NumberOfSeats > 4)
    .ProjectToDto()
    .ToListAsync();

Verifique capturando o SQL gerado (db.Cars...ToQueryString() ou o logging do EF Core) e confirmando que a projeção roda no banco de dados, não em memória. Note que o caminho de projeção do Mapperly não executa object factories nem métodos personalizados de criação de objetos, porque ele precisa produzir uma árvore de expressão traduzível.

7. Substitua o registro de DI

Apague o registro do AutoMapper e a varredura de assemblies:

// AutoMapper 15.0 - remove this
builder.Services.AddAutoMapper(typeof(CarProfile).Assembly);

Registre cada mapper explicitamente. Um mapper sem dependências injetadas não tem estado e é seguro para threads, então registre-o como singleton; um com uma dependência scoped precisa coincidir com esse ciclo de vida:

// .NET 11
builder.Services.AddSingleton<CarMapper>();        // no dependencies
builder.Services.AddScoped<InvoiceMapper>();       // depends on a scoped service

Depois mude os consumidores de IMapper para o mapper concreto. _mapper.Map<CarDto>(car) vira _carMapper.ToDto(car). Mappers estáticos não precisam de registro algum; chame CarQueryMapper.ProjectToDto(query) diretamente. Verifique que a aplicação inicializa: um registro faltante é agora uma InvalidOperationException de inicialização, que é exatamente a falha precoce que você quer.

8. Remova o AutoMapper

Quando o grep de using AutoMapper e CreateMap< não retornar nada, remova o pacote:

dotnet remove package AutoMapper

Verifique com um dotnet build limpo e uma execução completa de dotnet test.

Verificação

Rode este checklist depois que o último profile desaparecer:

Plano de rollback

Esta migração é reversível arquivo por arquivo, o que é sua principal propriedade de segurança. Como você manteve o AutoMapper referenciado durante o passo 7, qualquer mapper individual que se comporte mal pode ser revertido restaurando seu Profile e reapontando aquele único consumidor de volta para IMapper; os dois sistemas coexistem sem conflito. O único momento sem volta é o passo 8, remover o pacote. Não apague o AutoMapper até que cada consumidor esteja convertido e toda a suíte de testes esteja verde. Se você está nervoso, envie as conversões de mappers em uma versão e a remoção do pacote na seguinte.

Tropeços que tivemos

Se você quer entender como o gerador faz isso em tempo de compilação antes de confiar nele em produção, a mecânica é a mesma coberta em como escrever um gerador de código-fonte para INotifyPropertyChanged.

Fontes

Comments

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

< Voltar