Start Debugging

Von AutoMapper zu Source-Generator-Mapping mit Mapperly migrieren

Eine Schritt-fuer-Schritt-Checkliste, um Profiles, IMapper, ForMember und ProjectTo von AutoMapper 15 durch von Riok.Mapperly 4.3 generierte Mapper in .NET 11 zu ersetzen.

AutoMapper durch einen Source Generator zu ersetzen ist eine mechanische Refaktorierung Datei für Datei, keine Neuentwicklung. Für einen typischen Dienst mit 30-80 Mappings, verteilt über einige Profile-Klassen, planen Sie einen halben bis einen Tag ein: Jedes CreateMap<Source, Dest>() wird zu einer partial-Methode in einer [Mapper]-Klasse, Aufrufe von IMapper.Map<T> werden zu direkten Methodenaufrufen, und ProjectTo<T>() wird zu einer generierten IQueryable-Erweiterung. Was bricht, ist alles, was sich auf die Laufzeitflexibilität von AutoMapper stützte: dynamische Map(object)-Aufrufe, IValueResolver mit injizierten Diensten und die Registrierung per Assembly-Scan. Es lohnt sich, wenn Sie über AutoMappers Umsatzgrenze von 5.000.000 USD liegen und keine Lizenz kaufen wollen, wenn Sie möchten, dass Native AOT und Trimming funktionieren, oder wenn Sie möchten, dass nicht gemappte Eigenschaften die Kompilierung fehlschlagen lassen, anstatt zur Laufzeit stillschweigend verworfen zu werden.

Referenzierte Versionen: Dieser Leitfaden behandelt das Verlassen von AutoMapper 14.x (das letzte MIT-Release) und 15.0 (das erste Release mit dualer Reciprocal Public License 1.5 / kommerzieller Lizenz von Lucky Penny Software). Der Ersatz zielt auf <TargetFramework>net11.0</TargetFramework> mit dem .NET 11 SDK, C# 14 und Riok.Mapperly 4.3.1 (veröffentlicht am 2025-12-22) ab. Falls Sie noch entscheiden, ob Sie überhaupt wechseln, ist es dieselbe Anbietergeschichte wie bei MediatR; lesen Sie daher von MediatR zu einfacher Dependency Injection migrieren für den parallelen Fall. Dieser Artikel setzt voraus, dass die Entscheidung gefallen ist.

Warum Teams gerade jetzt AutoMapper verlassen

Was bricht

BereichÄnderungSchweregrad
IMapper-InjektionErsetzt durch die konkrete generierte Mapper-Klasse, direkt injizierthoch
Profile + CreateMap<,>()Kollabieren zu einer partiellen [Mapper]-Klasse mit einer partial-Methode pro Richtunghoch
ForMember(... MapFrom ...)Ersetzt durch [MapProperty] oder eine private Mapping-Methodehoch
ReverseMap()Keine automatische Umkehrung; Sie deklarieren die Rückrichtungsmethode explizitmittel
IValueResolver / ITypeConverter mit DIErsetzt durch einen Konstruktor am Mapper plus eine private Methodemittel
ProjectTo<T>(config)Ersetzt durch eine generierte IQueryable<T>-Projektionserweiterungmittel
AddAutoMapper(assembly)-ScanErsetzt durch explizite AddSingleton<TMapper>()-Registrierungmittel
mapper.Map(object, type) (dynamisch)Kein untypisierter Laufzeit-Einstiegspunkt; jedes Mapping ist eine typisierte Methodehoch
AssertConfigurationIsValid()-TestÜberflüssig; der Build ist die Zusicherungniedrig

Pre-Flight-Checkliste

  1. Installieren Sie das .NET 11 SDK und bestätigen Sie, dass dotnet --version 11.0.x meldet.
  2. Inventarisieren Sie Ihre Mappings. Führen Sie ein grep nach CreateMap< aus, um die Konfigurationen zu zählen, und nach \.Map< und ProjectTo<, um die Aufrufstellen zu zählen. Das ist der Umfang Ihrer Migration.
  3. Finden Sie die dynamischen Mappings. Suchen Sie per grep nach Map(-Aufrufen, die ein Type-Argument oder eine object-Quelle übergeben. Diese haben kein direktes Mapperly-Äquivalent und brauchen eine typisierte Methode oder einen manuellen Switch; kümmern Sie sich zuerst darum.
  4. Finden Sie die Resolver. Suchen Sie per grep nach IValueResolver, ITypeConverter und IMappingAction. Jeder braucht eine private Methode am Mapper.
  5. Kein spezielles Backup nötig, verzweigen Sie normal. Diese Änderung ist Datei für Datei reversibel (siehe Rollback-Plan), also genügt ein Feature-Branch.

Migrationsschritte

1. Fügen Sie Mapperly neben AutoMapper hinzu

Installieren Sie beide Pakete, um ein Profile nach dem anderen migrieren zu können:

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

Mapperly liefert nur einen Analyzer und die Attribute aus Riok.Mapperly.Abstractions, fügt also keine Laufzeitabhängigkeit hinzu. Stellen Sie sicher, dass der Build weiterhin gelingt: dotnet build ohne neue Fehler. Lassen Sie das AutoMapper-Paket referenziert, bis das letzte Profile verschwunden ist.

2. Konvertieren Sie ein einfaches Profile zu einer [Mapper]-Klasse

Nehmen Sie zuerst das kleinste Profile. Das Vorher:

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

Das Nachher ist eine partielle Klasse mit einer partiellen Methode. Mapperly füllt den Rumpf:

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

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

Verifizieren: Bauen Sie das Projekt und öffnen Sie die generierte Datei (sie erscheint in der Analyzer-Ausgabe, oder setzen Sie <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> in der .csproj, um sie auf die Festplatte zu schreiben). Bestätigen Sie, dass jede Eigenschaft von CarDto zugewiesen wird. Falls eine nicht zugewiesen wird, emittiert Mapperly eine Diagnose wie RMG020 für ein nicht gemapptes Zielmitglied; das ist die Funktion, kein Fehlschlag.

3. Übersetzen Sie ForMember zu [MapProperty]

AutoMappers Anpassung pro Mitglied bildet sich auf Mapperly-Attribute ab. Eine Umbenennung:

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

wird zu einem [MapProperty]-Attribut, das die Quell- und Zielmitglieder benennt:

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

Für einen berechneten Wert wird AutoMappers Inline-MapFrom-Lambda zu einer privaten Mapping-Methode, die per Use referenziert wird:

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

Um eine Eigenschaft bewusst zu verwerfen, ersetzen Sie AutoMappers Ignore() durch [MapperIgnoreTarget(nameof(CarDto.Internal))] oder [MapperIgnoreSource(nameof(Car.Secret))]. Verifizieren Sie jede Konvertierung, indem Sie prüfen, dass der generierte Rumpf genau die erwarteten Mitglieder zuweist.

4. Ersetzen Sie ReverseMap() durch eine explizite Methode

AutoMappers ReverseMap() ist ein einzelner Aufruf. Mapperly hat keine automatische Umkehrung, also deklarieren Sie die Rückrichtung als eigene Methode am selben 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);
}

Das ist mehr Code, aber ehrlich: Das umgekehrte Mapping ist nicht mehr implizit, sodass eine asymmetrische Eigenschaft (ein Zielfeld ohne Quelle) als eigene Diagnose auftaucht, statt stillschweigend ignoriert zu werden. Verifizieren Sie beide generierten Rümpfe.

5. Verschieben Sie Resolver mit Abhängigkeiten in den Konstruktor des Mappers

Ein AutoMapper-IValueResolver, der einen injizierten Dienst benötigt:

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

wird zu einem Konstruktor am Mapper plus einer privaten Methode. Mapperly generiert einen Konstruktor, der die von Ihnen deklarierten Parameter weiterleitet, sodass der Mapper an der normalen Konstruktorinjektion teilnimmt:

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

Verifizieren Sie, indem Sie den Mapper in einem Test aus dem Container auflösen und den formatierten Preis prüfen.

6. Konvertieren Sie ProjectTo<T> zu einer generierten Projektion

AutoMappers ProjectTo<T>() baut einen Expression-Baum, damit EF Core das Mapping in SQL übersetzen kann. Mapperly generiert dieselbe Art von IQueryable-Erweiterung, wenn Sie eine Methode deklarieren, die IQueryable<T> entgegennimmt und zurückgibt:

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

Die Aufrufstelle ändert sich von .ProjectTo<CarDto>(_config) zu .ProjectToDto():

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

Verifizieren Sie, indem Sie das generierte SQL erfassen (db.Cars...ToQueryString() oder das EF-Core-Logging) und bestätigen, dass die Projektion in der Datenbank läuft, nicht im Speicher. Beachten Sie, dass Mapperlys Projektionspfad keine Object Factories oder benutzerdefinierte objekterzeugende Methoden ausführt, da er einen übersetzbaren Ausdrucksbaum erzeugen muss.

7. Ersetzen Sie die DI-Registrierung

Löschen Sie die AutoMapper-Registrierung und den Assembly-Scan:

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

Registrieren Sie jeden Mapper explizit. Ein Mapper ohne injizierte Abhängigkeiten ist zustandslos und threadsicher, registrieren Sie ihn also als Singleton; einer mit einer scoped Abhängigkeit muss zu diesem Lebenszyklus passen:

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

Ändern Sie dann die Konsumenten von IMapper auf den konkreten Mapper. _mapper.Map<CarDto>(car) wird zu _carMapper.ToDto(car). Statische Mapper benötigen überhaupt keine Registrierung; rufen Sie CarQueryMapper.ProjectToDto(query) direkt auf. Verifizieren Sie, dass die App startet: Eine fehlende Registrierung ist nun eine InvalidOperationException beim Start, genau das frühe Fehlschlagen, das Sie wollen.

8. Entfernen Sie AutoMapper

Sobald das grep nach using AutoMapper und CreateMap< nichts mehr zurückgibt, entfernen Sie das Paket:

dotnet remove package AutoMapper

Verifizieren Sie mit einem sauberen dotnet build und einem vollständigen dotnet test-Lauf.

Verifizierung

Führen Sie diese Checkliste aus, nachdem das letzte Profile verschwunden ist:

Rollback-Plan

Diese Migration ist Datei für Datei reversibel, was ihre wichtigste Sicherheitseigenschaft ist. Da Sie AutoMapper bis Schritt 7 referenziert gelassen haben, kann jeder einzelne Mapper, der sich falsch verhält, rückgängig gemacht werden, indem Sie sein Profile wiederherstellen und diesen einen Konsumenten zurück auf IMapper umstellen; die beiden Systeme koexistieren ohne Konflikt. Der einzige Punkt ohne Wiederkehr ist Schritt 8, das Entfernen des Pakets. Löschen Sie AutoMapper nicht, bevor jeder Konsument konvertiert ist und die gesamte Testsuite grün ist. Wenn Sie nervös sind, liefern Sie die Mapper-Konvertierungen in einem Release und das Entfernen des Pakets im nächsten aus.

Stolpersteine, auf die wir trafen

Wenn Sie verstehen wollen, wie der Generator dies zur Kompilierzeit tut, bevor Sie ihm in der Produktion vertrauen, ist die Mechanik dieselbe wie in wie man einen Source Generator für INotifyPropertyChanged schreibt.

Quellen

Comments

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

< Zurück