C# PR

【C#】AutoMapperでデータマッピングを自動化!超簡単にできるプロパティマッピング術

【C#】AutoMapperでデータマッピングを自動化!超簡単にできるプロパティマッピング術
記事内に商品プロモーションを含む場合があります

こんにちは!

C#でシステム開発をしていると、よく直面するのが異なるクラス間でのプロパティのマッピング作業。

大量のプロパティを手動でマッピングするのが面倒。
コードが冗長になってしまう。
マッピングのメンテナンスが大変。

このような悩みを抱えている方も多いのではないでしょうか?

この記事では、C#の強力なマッピングライブラリ「AutoMapper」について、基本的な使い方から具体的な活用方法、さらには効果的な設定方法まで、詳しくご紹介します。

この記事は次のような人におすすめです。

この記事はこんな人におすすめ!
  • C#でのオブジェクトマッピングに時間がかかっている
  • AutoMapperの使い方を詳しく知りたい
  • マッピング処理をよりスマートに書きたい
  • AutoMapperの具体的な活用例が知りたい

この記事を読めば、AutoMapperの基本から応用までが分かるだけでなく、具体的なコード例を参考に、すぐに実装できるようになりますよ!
さらに、効率的なマッピング設定のコツもお伝えしています。

「C#でのマッピング処理を改善したい方」「AutoMapperの活用方法を知りたい方」は、ぜひ参考にしてください。

それでは、順を追って詳しく見ていきましょう!

そもそもAutoMapperとは?

まずは、AutoMapperについて簡単におさらいしておきましょう。

AutoMapperとは、C#のオブジェクト同士のプロパティをマッピング(コピー)するためのライブラリです。主な特徴としては、設定に従って自動的にオブジェクト間のプロパティをマッピングしてくれることや、カスタムマッピングルールの定義、コレクション型の変換なども行えることが挙げられます。

例えば、以下のようなデータベースのエンティティクラスとビューモデルクラスがあるとします。

// データベースのエンティティクラス
public class UserEntity
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string Email { get; set; }
}

// 画面表示用のビューモデルクラス
public class UserViewModel
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

このような異なるクラス間でのプロパティのマッピングを、従来は以下のように手動で行う必要がありました。

var user = new UserEntity
{
    Id = 1,
    FirstName = "John",
    LastName = "Doe",
    DateOfBirth = new DateTime(1990, 1, 1),
    Email = "john.doe@example.com"
};

var viewModel = new UserViewModel
{
    Id = user.Id,
    FullName = $"{user.FirstName} {user.LastName}",
    Age = DateTime.Now.Year - user.DateOfBirth.Year,
    Email = user.Email
};

しかし、AutoMapperを使用すると、以下のようにシンプルに書くことができます。

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<UserEntity, UserViewModel>()
       .ForMember(dest => dest.FullName,
                 opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
       .ForMember(dest => dest.Age,
                 opt => opt.MapFrom(src => DateTime.Now.Year - src.DateOfBirth.Year));
});

var mapper = config.CreateMapper();
var viewModel = mapper.Map<UserViewModel>(user);

このように、AutoMapperを使用することで、コードの量を大幅に削減でき、より保守性の高いコードを書くことができます。

AutoMapperを使用するメリット

AutoMapperを使用することには、実はたくさんのメリットがあります。主な5つのメリットについて詳しく解説します。

メリット1: コード量の削減

手動でマッピングを行う場合、プロパティの数だけコードを書く必要があります。しかし、AutoMapperを使用すると、共通の命名規則に従ったプロパティは自動的にマッピングされます。これにより、コード量を大幅に削減できます。

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<LargeEntity, LargeViewModel>();
});

var mapper = config.CreateMapper();
var viewModel = mapper.Map<LargeViewModel>(entity);

メリット2: 保守性の向上

プロパティの追加や変更があった場合、手動マッピングでは該当箇所を全て修正する必要があります。しかし、AutoMapperを使用していれば、多くの場合は設定を変更する必要すらありません。

例えば、エンティティクラスに新しいプロパティが追加された場合。

public class UserEntity
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string Email { get; set; }
    // 新しく追加されたプロパティ
    public string PhoneNumber { get; set; }
}

対応するビューモデルにも同名のプロパティを追加するだけで、AutoMapperは自動的にマッピングを行ってくれます。

public class UserViewModel
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
    // 追加されたプロパティ
    public string PhoneNumber { get; set; }
}

メリット3: 型変換の自動処理

AutoMapperは、多くの一般的な型変換を自動的に処理してくれます。例えば、数値型同士の変換(intからlongへの変換など)や、文字列と数値の変換、列挙型と文字列の変換などを自動的に行います。

public class Source
{
    public int Number { get; set; }
    public string Text { get; set; }
    public UserType UserType { get; set; }
}

public class Destination
{
    public long Number { get; set; }        // intからlongへの自動変換
    public int Text { get; set; }           // 文字列から数値への自動変換
    public string UserType { get; set; }    // 列挙型から文字列への自動変換
}

public enum UserType
{
    Admin,
    User,
    Guest
}

メリット4: テスト容易性

AutoMapperは、マッピング設定が正しいかを起動時にチェックする機能を提供しています。これにより、実行時のマッピングエラーを事前に防ぐことができます。マッピング設定に問題がある場合(例:存在しないプロパティへのマッピングを設定している場合)、AutoMapper.AutoMapperConfigurationExceptionが発生します。これにより、開発時に設定の誤りを早期に発見できます。

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Source, Destination>();
});

// マッピング設定が正しいかチェック
config.AssertConfigurationIsValid();

メリット5: カスタマイズの柔軟性

AutoMapperは、必要に応じて詳細なカスタマイズが可能です。条件付きマッピング、値の変換、プロパティの無視など、様々なカスタマイズオプションを提供しています。

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Source, Destination>()
        // 条件付きマッピング
        .ForMember(dest => dest.Status,
                  opt => opt.Condition(src => src.IsActive))
        // 値の変換
        .ForMember(dest => dest.FullName,
                  opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
        // プロパティの無視
        .ForMember(dest => dest.InternalId, opt => opt.Ignore());
});

これらのメリットを考えると、C#でのオブジェクトマッピングにAutoMapperを導入することは、生産性と保守性の向上につながる選択と言えるでしょう。

AutoMapperの基本的な使い方

AutoMapperの基本的な使い方について、具体的な手順を見ていきましょう。

インストール

まずは、NuGetパッケージマネージャーを使用してAutoMapperをインストールします。

Visual Studioのパッケージマネージャーコンソールで以下のコマンドを実行します。

Install-Package AutoMapper

または、.NET CLIを使用する場合。

dotnet add package AutoMapper

マッピング設定の定義

次に、マッピング設定を定義します。これは通常、アプリケーション起動時に一度だけ行います。AutoMapperでマッピング設定を行うには、必ずProfileクラスを継承したクラスを作成する必要があります

Profileクラスは、マッピング設定をグループ化し、整理するための基本クラスです。大規模なアプリケーションでは、複数のProfileクラスを作成して、機能やモジュールごとにマッピング設定を分けることができます。

using AutoMapper;

// Profileクラスを継承したマッピング設定クラス
public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<UserEntity, UserViewModel>();
        CreateMap<ProductEntity, ProductViewModel>();
        // 他のマッピング設定...
    }
}

// 必要に応じて、機能ごとにProfileを分けることもできます
public class OrderMappingProfile : Profile
{
    public OrderMappingProfile()
    {
        CreateMap<OrderEntity, OrderViewModel>();
        CreateMap<OrderItemEntity, OrderItemViewModel>();
    }
}

ASP.NET Coreを使用している場合は、Startup.csまたはProgram.csで依存性注入の設定を行います。

public void ConfigureServices(IServiceCollection services)
{
    services.AddAutoMapper(typeof(Startup));
    // または
    services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
}

マッピングの実行

設定が完了したら、以下のようにマッピングを実行できます。

// リポジトリのインターフェース
public interface IUserRepository
{
    UserEntity GetById(int id);
}

// コンストラクタインジェクションでIMapperとIRepositoryを注入
public class UserService
{
    private readonly IMapper _mapper;
    private readonly IUserRepository _repository;

    public UserService(IMapper mapper, IUserRepository repository)
    {
        _mapper = mapper;
        _repository = repository;
    }

    public UserViewModel GetUser(int id)
    {
        var user = _repository.GetById(id);
        return _mapper.Map<UserViewModel>(user);
    }
}

カスタムマッピングの定義

プロパティ名が異なる場合や、特別な変換が必要な場合は、カスタムマッピングを定義できます。

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<UserEntity, UserViewModel>()
            // プロパティ名が異なる場合
            .ForMember(dest => dest.FullName,
                      opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
            // 計算が必要な場合
            .ForMember(dest => dest.Age,
                      opt => opt.MapFrom(src => DateTime.Now.Year - src.DateOfBirth.Year))
            // 条件付きマッピング
            .ForMember(dest => dest.Status,
                      opt => opt.Condition(src => src.IsActive))
            // プロパティの無視
            .ForMember(dest => dest.InternalId,
                      opt => opt.Ignore());
    }
}

AutoMapperの具体的な活用例

DTOとエンティティの変換

データベースのエンティティとAPIのDTOを変換する具体的な例を見てみましょう。

// データベースエンティティ
public class OrderEntity
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime OrderDate { get; set; }
    public List<OrderItemEntity> Items { get; set; }
}

public class OrderItemEntity
{
    public int Id { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

// API DTO
public class OrderDto
{
    public int OrderId { get; set; }
    public string Customer { get; set; }
    public decimal Total { get; set; }
    public string OrderDate { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class OrderItemDto
{
    public string Product { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

// マッピング設定用のProfileクラス
public class OrderMappingProfile : Profile
{
    public OrderMappingProfile()
    {
        // 注文エンティティとDTOのマッピング
        CreateMap<OrderEntity, OrderDto>()
            .ForMember(dest => dest.OrderId, opt => opt.MapFrom(src => src.Id))
            .ForMember(dest => dest.Customer, opt => opt.MapFrom(src => src.CustomerName))
            .ForMember(dest => dest.Total, opt => opt.MapFrom(src => src.TotalAmount))
            .ForMember(dest => dest.OrderDate, opt => opt.MapFrom(src => src.OrderDate.ToString("yyyy-MM-dd")));

        // 注文項目エンティティとDTOのマッピング
        CreateMap<OrderItemEntity, OrderItemDto>()
            .ForMember(dest => dest.Product, opt => opt.MapFrom(src => src.ProductName))
            .ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.UnitPrice));
    }
}

この例では、以下のような変換が自動的に行われます。

異なるプロパティ名のマッピング
ForMemberを使用して、異なる名前のプロパティ間でマッピングを定義しています。
型変換の自動処理
DateTimeからstringへの変換など、必要な型変換を自動的に処理します。
コレクションの自動マッピング
List<OrderItemEntity>からList<OrderItemDto>への変換も自動的に行われます。

継承関係のあるクラスのマッピング

継承関係のあるクラス間でのマッピングの例を見てみましょう。

// ベースクラス
public abstract class PersonBase
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// 派生クラス(社員)
public class Employee : PersonBase
{
    public string EmployeeNumber { get; set; }
    public string Department { get; set; }
    public decimal Salary { get; set; }
}

// 派生クラス(顧客)
public class Customer : PersonBase
{
    public string CustomerCode { get; set; }
    public int LoyaltyPoints { get; set; }
}

// DTOクラス
public class PersonDto
{
    public int Id { get; set; }
    public string FullName { get; set; }
}

public class EmployeeDto : PersonDto
{
    public string Department { get; set; }
    public string Number { get; set; }  // EmployeeNumberと異なる名前
}

public class CustomerDto : PersonDto
{
    public string Code { get; set; }    // CustomerCodeと異なる名前
    public int Points { get; set; }     // LoyaltyPointsと異なる名前
}

// マッピング設定
public class PersonMappingProfile : Profile
{
    public PersonMappingProfile()
    {
        // ベースクラスのマッピング
        CreateMap<PersonBase, PersonDto>()
            .ForMember(dest => dest.FullName,
                      opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
            .Include<Employee, EmployeeDto>()    // 派生クラスのマッピングを含める
            .Include<Customer, CustomerDto>();    // 派生クラスのマッピングを含める

        // Employeeのマッピング
        CreateMap<Employee, EmployeeDto>()
            .ForMember(dest => dest.Number, opt => opt.MapFrom(src => src.EmployeeNumber));

        // Customerのマッピング
        CreateMap<Customer, CustomerDto>()
            .ForMember(dest => dest.Code, opt => opt.MapFrom(src => src.CustomerCode))
            .ForMember(dest => dest.Points, opt => opt.MapFrom(src => src.LoyaltyPoints));
    }
}

この例では、以下のような継承関係でのマッピングを実現しています。

基本クラスのマッピング
PersonBaseからPersonDtoへのマッピングを定義し、共通のプロパティ変換(フルネームの生成など)を設定します。
Include句の使用
Includeメソッドを使用して、派生クラスのマッピングを基本クラスのマッピングに含めることを宣言します。
派生クラス固有のマッピング
各派生クラスで独自に持つプロパティについて、個別にマッピング設定を行います。

深いネストを持つオブジェクトのマッピング

複雑なネスト構造を持つオブジェクトのマッピング例を見てみましょう。

// ソースクラス(データベースエンティティ)
public class CompanyEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public AddressEntity MainAddress { get; set; }
    public List<DepartmentEntity> Departments { get; set; }
}

public class AddressEntity
{
    public string Street { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
    public string Country { get; set; }
}

public class DepartmentEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<EmployeeEntity> Employees { get; set; }
}

public class EmployeeEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
}

// 出力用DTOクラス
public class CompanyDto
{
    public int CompanyId { get; set; }
    public string CompanyName { get; set; }
    public AddressDto Address { get; set; }
    public List<DepartmentSummaryDto> Departments { get; set; }
    public int TotalEmployees { get; set; }  // 計算項目
}

public class AddressDto
{
    public string FullAddress { get; set; }  // 住所を1行に結合
}

public class DepartmentSummaryDto
{
    public string Name { get; set; }
    public int EmployeeCount { get; set; }   // 計算項目
    public List<string> EmployeeNames { get; set; }  // 変換項目
}

// マッピング設定
public class CompanyMappingProfile : Profile
{
    public CompanyMappingProfile()
    {
        CreateMap<CompanyEntity, CompanyDto>()
            .ForMember(dest => dest.CompanyId, opt => opt.MapFrom(src => src.Id))
            .ForMember(dest => dest.CompanyName, opt => opt.MapFrom(src => src.Name))
            .ForMember(dest => dest.TotalEmployees,
                      opt => opt.MapFrom(src => src.Departments.Sum(d => d.Employees.Count)));

        CreateMap<AddressEntity, AddressDto>()
            .ForMember(dest => dest.FullAddress,
                      opt => opt.MapFrom(src =>
                          $"{src.Street}, {src.City}, {src.PostalCode}, {src.Country}"));

        CreateMap<DepartmentEntity, DepartmentSummaryDto>()
            .ForMember(dest => dest.EmployeeCount,
                      opt => opt.MapFrom(src => src.Employees.Count))
            .ForMember(dest => dest.EmployeeNames,
                      opt => opt.MapFrom(src => src.Employees.Select(e => e.Name).ToList()));
    }
}

この例では、以下のような高度なマッピング機能を活用しています。

ネストされたオブジェクトの自動マッピング
MainAddressDepartmentsのような入れ子構造も自動的にマッピングされます。
計算項目のマッピング
TotalEmployeesEmployeeCountのような、元のデータから計算して生成する項目を定義できます。
コレクションの変換
EmployeesリストからEmployeeNamesという文字列リストへの変換など、コレクションの型変換も可能です。
文字列の結合
住所情報を1行の文字列に結合するなど、複数のフィールドを組み合わせた変換も可能です。

AutoMapperのベストプラクティス

適切な単位でのProfile分割

マッピング設定は、機能やモジュールごとに適切な単位でProfileを分割することをおすすめします。

// ユーザー関連のマッピング
public class UserMappingProfile : Profile
{
    public UserMappingProfile()
    {
        CreateMap<UserEntity, UserDto>();
        CreateMap<UserPreferences, UserPreferencesDto>();
    }
}

// 注文関連のマッピング
public class OrderMappingProfile : Profile
{
    public OrderMappingProfile()
    {
        CreateMap<OrderEntity, OrderDto>();
        CreateMap<OrderItem, OrderItemDto>();
    }
}

// 商品関連のマッピング
public class ProductMappingProfile : Profile
{
    public ProductMappingProfile()
    {
        CreateMap<ProductEntity, ProductDto>();
        CreateMap<CategoryEntity, CategoryDto>();
    }
}

バリデーションの活用

マッピング設定が正しいかどうかを、起動時に検証することをおすすめします。

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // 開発環境でのみマッピング設定を検証
        if (Environment.IsDevelopment())
        {
            var mapper = app.ApplicationServices.GetRequiredService<IMapper>();
            mapper.ConfigurationProvider.AssertConfigurationIsValid();
        }
    }
}

カスタム値変換の定義

頻繁に使用する変換ロジックは、カスタム値変換として定義することで、再利用性を高めることができます。

// カスタム値変換の定義
public class DateTimeToStringConverter : IValueConverter<DateTime, string>
{
    public string Convert(DateTime source, ResolutionContext context)
        => source.ToString("yyyy-MM-dd HH:mm:ss");
}

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        // 値変換の利用
        CreateMap<UserEntity, UserDto>()
            .ForMember(dest => dest.CreatedAt,
                      opt => opt.ConvertUsing(new DateTimeToStringConverter()));

        // または、インラインで定義
        CreateMap<OrderEntity, OrderDto>()
            .ForMember(dest => dest.OrderDate,
                      opt => opt.ConvertUsing(new DateTimeToStringConverter()));
    }
}

条件付きマッピングの活用

特定の条件に基づいてマッピングを制御することで、より柔軟で安全なマッピングを実現できます。

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<Product, ProductDto>()
            // nullチェック
            .ForMember(dest => dest.CategoryName,
                      opt => opt.MapFrom(src => src.Category != null ? src.Category.Name : "未分類"))
            // 値の条件チェック
            .ForMember(dest => dest.PriceRange,
                      opt => opt.MapFrom(src =>
                          src.Price < 1000 ? "低価格" :
                          src.Price < 5000 ? "中価格" : "高価格"))
            // 条件に基づくマッピングのスキップ
            .ForMember(dest => dest.DiscountPrice,
                      opt => opt.Condition(src => src.IsOnSale));
    }
}

テスト容易性の確保

マッピング設定は、単体テストで検証することをおすすめします。

public class MappingTests
{
    private readonly IMapper _mapper;

    public MappingTests()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.AddMaps(typeof(Startup).Assembly);
        });
        _mapper = config.CreateMapper();
    }

    [Fact]
    public void ShouldMapUserEntityToUserDto()
    {
        // Arrange
        var user = new UserEntity
        {
            Id = 1,
            FirstName = "John",
            LastName = "Doe",
            Email = "john.doe@example.com"
        };

        // Act
        var dto = _mapper.Map<UserDto>(user);

        // Assert
        Assert.Equal(user.Id, dto.Id);
        Assert.Equal($"{user.FirstName} {user.LastName}", dto.FullName);
        Assert.Equal(user.Email, dto.Email);
    }
}

これらのベストプラクティスを適用することで、より保守性が高く、パフォーマンスの良いコードを書くことができます。

キャリア形成/給与還元
ひとつひとつ真摯に向き合う企業
ONE_WEDGE社員募集

株式会社 ONE WEDGEでは、新たな仲間を募集しています!

私たちと一緒に、革新的で充実したキャリアを築きませんか?
当社は、従業員が仕事と私生活のバランスを大切にできるよう、充実した福利厚生を整えています。

  • 完全週休2日制(土日休み)で、祝日や夏季休暇、年末年始休暇もしっかり保証!
  • 様々な休暇制度(有給、慶弔、産前・産後、育児、バースデー休暇)を完備!
  • 従業員の成長と健康を支援するための表彰制度、資格取得支援、健康促進手当など!
  • 生活を支えるテレワーク手当、記事寄稿手当、結婚祝金・出産祝金など、様々な手当を提供!
  • 自己啓発としての書籍購入制度や、メンバー間のコミュニケーションを深める交流費補助!
  • 成果に応じた決算賞与や、リファラル採用手当、AI手当など、頑張りをしっかり評価!
  • ワークライフバランスを重視し、副業もOK!

株式会社 ONE WEDGEでは、一人ひとりの従業員が自己実現できる環境を大切にしています。
共に成長し、刺激を与え合える仲間をお待ちしております。
あなたの能力と熱意を、ぜひ当社で発揮してください。
ご応募お待ちしております!

ホームページ、採用情報は下記ボタンからご確認ください!

応募、ご質問など、LINEでお気軽にご相談ください♪

まとめ

ここまで、C#のAutoMapperについて詳しく解説してきました。改めて、重要なポイントをおさらいしましょう。

  • AutoMapperは強力なマッピングライブラリ
  • Profileクラスを使用して設定を整理できる
  • 基本的なプロパティは自動でマッピング
  • カスタムマッピングで柔軟な変換が可能
  • 適切な設定で保守性の高いコードを実現
  • テストを活用して信頼性を確保
  • ベストプラクティスを意識した実装が重要

AutoMapperを使用することで、以下のような利点を得ることができます。

  • コードの量を大幅に削減できる
  • 保守性の高いマッピング処理を実現できる
  • 型変換を自動的に処理できる
  • テストが容易になる
  • カスタマイズの自由度が高い

この記事で紹介した方法を参考に、ぜひAutoMapperを活用してみてください。マッピング処理の効率が大幅に向上し、より良いコードが書けるようになるはずです。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です