Start Debugging

Migrar de AutoMapper al mapeo generado por código fuente con Mapperly

Una lista de verificacion paso a paso para reemplazar los Profiles, IMapper, ForMember y ProjectTo de AutoMapper 15 por mappers generados por codigo fuente con Riok.Mapperly 4.3 en .NET 11.

Reemplazar AutoMapper por un generador de código fuente es una refactorización mecánica, archivo por archivo, no una reescritura. Para un servicio típico con 30-80 mapeos repartidos en unas pocas clases Profile, calcula de medio día a un día: cada CreateMap<Source, Dest>() se convierte en un método partial de una clase [Mapper], las llamadas a IMapper.Map<T> pasan a ser llamadas directas a métodos, y ProjectTo<T>() se convierte en una extensión IQueryable generada. Lo que se rompe es todo lo que dependía de la flexibilidad en runtime de AutoMapper: las llamadas dinámicas Map(object), los IValueResolver con servicios inyectados, y el registro por escaneo de ensamblados. Vale la pena hacerlo si estás por encima del límite de ingresos de 5.000.000 USD de AutoMapper y no quieres comprar una licencia, si quieres que Native AOT y el trimming funcionen, o si quieres que las propiedades no mapeadas fallen la compilación en lugar de descartarse silenciosamente en runtime.

Versiones referenciadas: esta guía cubre dejar AutoMapper 14.x (la última versión MIT) y 15.0 (la primera versión con doble licencia Reciprocal Public License 1.5 / comercial de Lucky Penny Software). El reemplazo apunta a <TargetFramework>net11.0</TargetFramework> con el SDK de .NET 11, C# 14 y Riok.Mapperly 4.3.1 (publicada el 2025-12-22). Si todavía estás decidiendo si irte, es la misma historia de proveedor que MediatR, así que lee migrar de MediatR a inyección de dependencias simple para el caso paralelo; este artículo asume que la decisión ya está tomada.

Por qué los equipos dejan AutoMapper justo ahora

Qué se rompe

ÁreaCambioSeveridad
Inyección de IMapperReemplazada por la clase mapper generada concreta, inyectada directamentealta
Profile + CreateMap<,>()Colapsan en una clase parcial [Mapper] con un método partial por direcciónalta
ForMember(... MapFrom ...)Reemplazado por [MapProperty] o un método de mapeo privadoalta
ReverseMap()No hay reverso automático; declaras el método de la dirección de retorno explícitamentemedia
IValueResolver / ITypeConverter con DIReemplazados por un constructor en el mapper más un método privadomedia
ProjectTo<T>(config)Reemplazado por una extensión de proyección IQueryable<T> generadamedia
Escaneo AddAutoMapper(assembly)Reemplazado por registro explícito AddSingleton<TMapper>()media
mapper.Map(object, type) (dinámico)No hay punto de entrada sin tipo en runtime; cada mapeo es un método tipadoalta
Prueba AssertConfigurationIsValid()Redundante; la compilación es la aserciónbaja

Lista de verificación previa

  1. Instala el SDK de .NET 11 y confirma que dotnet --version reporta 11.0.x.
  2. Inventaría tus mapeos. Ejecuta un grep de CreateMap< para contar las configuraciones y de \.Map< y ProjectTo< para contar los puntos de llamada. Ese es el alcance de tu migración.
  3. Encuentra los mapeos dinámicos. Busca con grep las llamadas Map( que pasan un argumento Type o un origen object. Estas no tienen equivalente directo en Mapperly y necesitan un método tipado o un switch manual; ocúpate de ellas primero.
  4. Encuentra los resolvers. Busca con grep IValueResolver, ITypeConverter e IMappingAction. Cada uno necesita un método privado en el mapper.
  5. No hace falta respaldo especial, ramifica normalmente. Este cambio es reversible archivo por archivo (ver Plan de reversión), así que una rama de funcionalidad basta.

Pasos de la migración

1. Agrega Mapperly junto a AutoMapper

Instala ambos paquetes para poder migrar un profile a la vez:

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

Mapperly entrega solo un analizador y los atributos de Riok.Mapperly.Abstractions, así que no agrega ninguna dependencia en runtime. Verifica que la compilación siga funcionando: dotnet build sin errores nuevos. Deja el paquete de AutoMapper referenciado hasta que desaparezca el último profile.

2. Convierte un profile simple a una clase [Mapper]

Toma primero el profile más pequeño. El antes:

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

El después es una clase parcial con un método parcial. Mapperly rellena el cuerpo:

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

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

Verifica: compila el proyecto y abre el archivo generado (aparece en la salida del analizador, o pon <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> en el .csproj para escribirlo en disco). Confirma que cada propiedad de CarDto se asigne. Si alguna no se asigna, Mapperly emite un diagnóstico como RMG020 para un miembro de destino no mapeado; eso es la característica, no un fallo.

3. Traduce ForMember a [MapProperty]

La personalización por miembro de AutoMapper se mapea a atributos de Mapperly. Un renombrado:

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

se convierte en un atributo [MapProperty] que nombra los miembros de origen y destino:

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

Para un valor calculado, la lambda MapFrom en línea de AutoMapper se convierte en un método de mapeo 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 una propiedad deliberadamente, reemplaza el Ignore() de AutoMapper por [MapperIgnoreTarget(nameof(CarDto.Internal))] o [MapperIgnoreSource(nameof(Car.Secret))]. Verifica cada conversión revisando que el cuerpo generado asigne exactamente los miembros que esperas.

4. Reemplaza ReverseMap() por un método explícito

El ReverseMap() de AutoMapper es una sola llamada. Mapperly no tiene reverso automático, así que declara la dirección de retorno como su propio método en el mismo 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);
}

Es más código, pero es honesto: el mapeo inverso ya no está implícito, así que una propiedad asimétrica (un campo de destino sin origen) aparece como su propio diagnóstico en lugar de ignorarse silenciosamente. Verifica ambos cuerpos generados.

5. Mueve los resolvers con dependencias al constructor del mapper

Un IValueResolver de AutoMapper que necesita un servicio inyectado:

// 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);
}

se convierte en un constructor en el mapper más un método privado. Mapperly genera un constructor que reenvía los parámetros que declaras, así que el mapper participa en la inyección de constructor 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);
}

Verifica resolviendo el mapper desde el contenedor en una prueba y comprobando el precio formateado.

6. Convierte ProjectTo<T> a una proyección generada

El ProjectTo<T>() de AutoMapper construye un árbol de Expression para que EF Core pueda traducir el mapeo a SQL. Mapperly genera el mismo tipo de extensión IQueryable cuando declaras un método que toma y devuelve 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);
}

El punto de llamada cambia de .ProjectTo<CarDto>(_config) a .ProjectToDto():

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

Verifica capturando el SQL generado (db.Cars...ToQueryString() o el logging de EF Core) y confirmando que la proyección se ejecuta en la base de datos, no en memoria. Ten en cuenta que el camino de proyección de Mapperly no ejecuta object factories ni métodos personalizados de creación de objetos, porque debe producir un árbol de expresión traducible.

7. Reemplaza el registro de DI

Elimina el registro de AutoMapper y el escaneo de ensamblados:

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

Registra cada mapper explícitamente. Un mapper sin dependencias inyectadas no tiene estado y es seguro para hilos, así que regístralo como singleton; uno con una dependencia scoped debe coincidir con ese ciclo de vida:

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

Luego cambia los consumidores de IMapper al mapper concreto. _mapper.Map<CarDto>(car) se convierte en _carMapper.ToDto(car). Los mappers estáticos no necesitan registro alguno; llama a CarQueryMapper.ProjectToDto(query) directamente. Verifica que la app arranque: un registro faltante es ahora una InvalidOperationException de arranque, que es exactamente el fallo temprano que quieres.

8. Elimina AutoMapper

Una vez que el grep de using AutoMapper y CreateMap< no devuelva nada, quita el paquete:

dotnet remove package AutoMapper

Verifica con un dotnet build limpio y una ejecución completa de dotnet test.

Verificación

Ejecuta esta lista de verificación después de que desaparezca el último profile:

Plan de reversión

Esta migración es reversible archivo por archivo, lo cual es su principal propiedad de seguridad. Como mantuviste AutoMapper referenciado durante el paso 7, cualquier mapper individual que se porte mal puede revertirse restaurando su Profile y volviendo a apuntar ese único consumidor a IMapper; los dos sistemas coexisten sin conflicto. El único momento de ida sin vuelta es el paso 8, quitar el paquete. No elimines AutoMapper hasta que cada consumidor esté convertido y toda la suite de pruebas esté en verde. Si estás nervioso, envía las conversiones de mappers en una versión y la eliminación del paquete en la siguiente.

Tropiezos que tuvimos

Si quieres entender cómo el generador hace esto en tiempo de compilación antes de confiar en él en producción, la mecánica es la misma que se cubre en cómo escribir un generador de código fuente para INotifyPropertyChanged.

Fuentes

Comments

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

< Volver