Start Debugging

AutoMapper から Mapperly のソース生成マッピングへ移行する

AutoMapper 15 の Profile、IMapper、ForMember、ProjectTo を .NET 11 の Riok.Mapperly 4.3 が生成するマッパーに置き換えるためのステップバイステップのチェックリスト。

AutoMapper をソースジェネレーターに置き換えるのは、書き直しではなく、ファイルごとの機械的なリファクタリングです。いくつかの Profile クラスに 30-80 件のマッピングが散らばった典型的なサービスなら、半日から 1 日を見込んでください。各 CreateMap<Source, Dest>()[Mapper] クラスの partial メソッドになり、IMapper.Map<T> の呼び出しは直接のメソッド呼び出しになり、ProjectTo<T>() は生成された IQueryable 拡張になります。壊れるのは、AutoMapper の実行時の柔軟性に依存していたものすべてです。動的な Map(object) 呼び出し、注入されたサービスを持つ IValueResolver、そしてアセンブリスキャンによる登録です。これを行う価値があるのは、AutoMapper の売上高 5,000,000 USD のラインを超えていてライセンスを購入したくない場合、Native AOT とトリミングを機能させたい場合、あるいはマッピングされていないプロパティを実行時に黙って破棄するのではなくビルドで失敗させたい場合です。

参照するバージョン: このガイドは、AutoMapper 14.x(最後の MIT リリース)と 15.0(Lucky Penny Software による Reciprocal Public License 1.5 / 商用のデュアルライセンスの最初のリリース)からの離脱を扱います。置き換え先は <TargetFramework>net11.0</TargetFramework> で、.NET 11 SDK、C# 14、Riok.Mapperly 4.3.1(2025-12-22 にリリース)を対象とします。そもそも離脱するかどうかをまだ検討している場合、これは MediatR と同じベンダーの話なので、並行するケースとして MediatR から素の依存性注入への移行 を読んでください。この記事は決定が済んでいることを前提とします。

なぜチームは今まさに AutoMapper を離れるのか

何が壊れるか

領域変更深刻度
IMapper の注入具体的な生成済みマッパークラスを直接注入する形に置き換え
Profile + CreateMap<,>()方向ごとに 1 つの partial メソッドを持つ partial な [Mapper] クラスに集約
ForMember(... MapFrom ...)[MapProperty] またはプライベートなマッピングメソッドに置き換え
ReverseMap()自動の逆変換はない。戻り方向のメソッドを明示的に宣言する
DI ありの IValueResolver / ITypeConverterマッパーのコンストラクターとプライベートメソッドに置き換え
ProjectTo<T>(config)生成された IQueryable<T> 射影拡張に置き換え
AddAutoMapper(assembly) スキャン明示的な AddSingleton<TMapper>() 登録に置き換え
mapper.Map(object, type)(動的)実行時の型なしエントリーポイントはない。各マッピングは型付きメソッド
AssertConfigurationIsValid() テスト冗長。ビルドがアサーションになる

事前チェックリスト

  1. .NET 11 SDK をインストール し、dotnet --version11.0.x を報告することを確認します。
  2. マッピングを棚卸しする。 CreateMap< を grep して構成の数を数え、\.Map<ProjectTo< を grep して呼び出し箇所の数を数えます。これが移行の範囲です。
  3. 動的なマッピングを見つける。 Type 引数または object のソースを渡す Map( 呼び出しを grep します。これらには Mapperly の直接の同等物がなく、型付きメソッドか手動の switch が必要です。まずこれらに対処してください。
  4. リゾルバーを見つける。 IValueResolverITypeConverterIMappingAction を grep します。それぞれにマッパー上のプライベートメソッドが必要です。
  5. 特別なバックアップは不要、通常どおりブランチを作成する。 この変更はファイルごとに元に戻せる(ロールバック計画を参照)ため、機能ブランチで十分です。

移行の手順

1. AutoMapper と並べて Mapperly を追加する

1 つずつ profile を移行できるよう、両方のパッケージをインストールします。

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

Mapperly はアナライザーと Riok.Mapperly.Abstractions の属性のみを提供するため、ランタイムの依存関係を追加しません。ビルドが引き続き成功することを確認してください。新しいエラーなしで dotnet build が通ること。最後の profile がなくなるまで、AutoMapper パッケージは参照したままにします。

2. 単純な profile を [Mapper] クラスに変換する

まず最小の profile を取り上げます。変更前:

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

変更後は partial メソッドを持つ partial クラスです。Mapperly が本体を埋めます。

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

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

検証: プロジェクトをビルドし、生成されたファイルを開きます(アナライザーの出力に現れます。あるいは .csproj<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> を設定してディスクに書き出します)。CarDto のすべてのプロパティが代入されることを確認してください。代入されないものがあれば、Mapperly はマッピングされていないターゲットメンバーに対して RMG020 のような診断を出力します。それは失敗ではなく機能です。

3. ForMember[MapProperty] に変換する

AutoMapper のメンバーごとのカスタマイズは Mapperly の属性に対応します。名前の変更:

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

は、ソースとターゲットのメンバーを指定する [MapProperty] 属性になります。

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

計算値の場合、AutoMapper のインラインの MapFrom ラムダは、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");
}

プロパティを意図的に破棄するには、AutoMapper の Ignore()[MapperIgnoreTarget(nameof(CarDto.Internal))] または [MapperIgnoreSource(nameof(Car.Secret))] に置き換えます。生成された本体が期待どおりのメンバーだけを代入することを確認して、各変換を検証してください。

4. ReverseMap() を明示的なメソッドに置き換える

AutoMapper の ReverseMap() は 1 回の呼び出しです。Mapperly には自動の逆変換がないため、戻り方向を同じマッパー上の独自のメソッドとして宣言します。

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

これはコード量が増えますが、正直です。逆マッピングはもはや暗黙ではないため、非対称なプロパティ(ソースのないターゲットフィールド)は黙って無視されるのではなく、独自の診断として現れます。両方の生成された本体を検証してください。

5. 依存関係を持つリゾルバーをマッパーのコンストラクターに移す

注入されたサービスを必要とする AutoMapper の IValueResolver:

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

は、マッパーのコンストラクターとプライベートメソッドになります。Mapperly は、宣言したパラメーターを転送するコンストラクターを生成するため、マッパーは通常のコンストラクター注入に参加します。

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

テストでコンテナーからマッパーを解決し、整形された価格を確認して検証してください。

6. ProjectTo<T> を生成された射影に変換する

AutoMapper の ProjectTo<T>() は、EF Core がマッピングを SQL に変換できるように Expression ツリーを構築します。Mapperly は、IQueryable<T> を受け取って返すメソッドを宣言すると、同じ種類の IQueryable 拡張を生成します。

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

呼び出し箇所は .ProjectTo<CarDto>(_config) から .ProjectToDto() に変わります。

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

生成された SQL を取得し(db.Cars...ToQueryString() または EF Core のログ出力)、射影がメモリ内ではなくデータベースで実行されることを確認して検証してください。Mapperly の射影パスは、変換可能な式ツリーを生成しなければならないため、object factory やカスタムのオブジェクト生成メソッドを実行しないことに注意してください。

7. DI の登録を置き換える

AutoMapper の登録とアセンブリスキャンを削除します。

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

各マッパーを明示的に登録します。注入された依存関係を持たないマッパーはステートレスでスレッドセーフなので、singleton として登録します。scoped な依存関係を持つマッパーは、そのライフタイムに合わせる必要があります。

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

次に、コンシューマーを IMapper から具体的なマッパーに変更します。_mapper.Map<CarDto>(car)_carMapper.ToDto(car) になります。静的マッパーは登録がまったく不要です。CarQueryMapper.ProjectToDto(query) を直接呼び出してください。アプリが起動することを確認します。登録の欠落は、いまや起動時の InvalidOperationException になります。これはまさに望ましい早期の失敗です。

8. AutoMapper を削除する

using AutoMapperCreateMap< の grep が何も返さなくなったら、パッケージを削除します。

dotnet remove package AutoMapper

クリーンな dotnet build と完全な dotnet test の実行で検証してください。

検証

最後の profile がなくなったら、このチェックリストを実行します。

ロールバック計画

この移行はファイルごとに元に戻せます。これが主な安全性の特性です。手順 7 まで AutoMapper を参照したままにしておいたため、誤動作する個々のマッパーは、その Profile を復元し、その 1 つのコンシューマーを IMapper に戻すことで元に戻せます。2 つのシステムは競合なく共存します。唯一の不可逆な瞬間は、手順 8 のパッケージの削除です。すべてのコンシューマーが変換され、テストスイート全体が緑になるまで、AutoMapper を削除しないでください。不安なら、マッパーの変換を 1 つのリリースで、パッケージの削除を次のリリースで出荷してください。

ぶつかった落とし穴

本番環境で信頼する前に、ジェネレーターがこれをコンパイル時にどう行うかを理解したい場合、その仕組みは INotifyPropertyChanged 向けのソースジェネレーターの書き方 で扱っているものと同じです。

出典

Comments

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

< 戻る