連載
» 2015年03月24日 11時17分 UPDATE

.NET TIPS:AutoMapperを使って異なるオブジェクト間のデータコピーを自動化するには?(独自マッピング編)

AutoMapperを使って、名前の異なるプロパティ/型が異なるプロパティなどの間でのデータコピーを自動化する方法を解説する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Platform Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 4以降


 オープンソースのライブラリ「AutoMapper」を使うと、異なるクラス間のデータコピーを自動化できる。「基本編」ではプロパティ名が同じ場合を紹介したが、プロパティ名が異なる場合はどうなのだろうか? 独自のマッピングを定義することで、その場合もデータコピーを自動化できる。さらには、データをコピーする際に型変換などの処理を実行させることもできるのだ。本稿では、そのようなAutoMapperのマッピング機能の使い方を紹介する。

 なお、本稿執筆時点でAutoMapperがサポートしているのは.NET Framework 4以降であるが、NuGetから導入するため、本稿ではVisual Studio 2012を使って説明する。また、本稿のサンプルは「MSDN Code Recipe:.NET Tips #1103」からダウンロードできる。

AutoMapperを導入するには?

 「.NET TIPS:AutoMapperを使ってオブジェクト間のデータコピーを自動化するには?(基本編)」をご覧いただきたい。

例題

 ここでは例題として、催事の情報を格納する「EventData」クラスと「EventDataForDisplay」クラスという二つのクラスを考えてみよう。この二つのクラスのオブジェクト間で、次の図のようなデータコピーを行いたいものとする。

本稿で実装するデータマッピング 本稿で実装するデータマッピング
AutoMapperでマッピングを定義することにより、この図に示したようなコピーが可能である。
  • Titleプロパティ→EventTitleプロパティ: プロパティ名が異なる。
  • DateTimeプロパティ→EventDateプロパティ/EventTimeプロパティ: プロパティ名が異なるだけでなく、プロパティの型も違う(DateTime型→String型)。さらに、データをコピーする際に、DateTime型から日付だけ(または、時刻だけ)を取り出す処理も行う。
  • GetFormattedDateTimeメソッド→FormattedDateTimeプロパティ: コピー元のメソッドを呼び出して得られたデータを、コピー先のプロパティにセットする。ここではメソッドの名前がプロパティ名の先頭に「Get」を付けた形になっている。

 「EventData」クラスはロジックで使うオブジェクトだとする。ロジックで使うオブジェクトは、シンプルなものにしたい。例えば次のコードのようになる。

using System;
using System.Collections.Generic;
namespace dotNetTips1103VS2012
{
  public class EventData
  {
    public string Title { get; set; }
    public DateTime DateTime { get; set; }
    public string GetFormattedDateTime()
    {
      return DateTime.ToString("yyyy/MM/dd HH:mm");
    }

    public static IList<EventData> GetData()
    {
      var list = new List<EventData>();
      // ダミーのデータ(実際にはデータベースなどから取得してくると思ってほしい)
      list.Add(new EventData() { Title="とっておきイベント その1",
                                 DateTime=new DateTime(2015,3,24,15,0,0), });
      list.Add(new EventData() { Title = "とっておきイベント その2"
                                 DateTime = new DateTime(2015, 4, 30, 0, 30, 0), });
      list.Add(new EventData() { Title = "とっておきイベント その3"
                                 DateTime = new DateTime(2015, 5, 26, 10, 0, 0), });
      return list;
    }
  }
}

Public Class EventData

  Public Property Title As String
  Public Property DateTime As DateTime
  Public Function GetFormattedDateTime() As String
    Return DateTime.ToString("yyyy/MM/dd HH:mm")
  End Function

  Public Shared Function GetData() As IList(Of EventData)
    Dim list = New List(Of EventData)()
    ' ダミーのデータ(実際にはデータベースなどから取得してくると思ってほしい)
    list.Add(New EventData() With {.Title = "とっておきイベント その1",
                                   .DateTime = New DateTime(2015, 3, 24, 15, 0, 0)})
    list.Add(New EventData() With {.Title = "とっておきイベント その2",
                                   .DateTime = New DateTime(2015, 4, 30, 0, 30, 0)})
    list.Add(New EventData() With {.Title = "とっておきイベント その3",
                                   .DateTime = New DateTime(2015, 5, 26, 10, 0, 0)})
    Return list
  End Function
End Class

ロジックで使う「EventData」クラスの例(上:C#、下:VB)
「Title」と「DateTime」という二つのプロパティおよび書式化された日時を返す「GetFormattedDateTime」メソッドを持つシンプルなクラスである。
また、何らかの方法でデータを取得してくる「GetData」メソッドがある(このクラスに配置することには異論があるかもしれないが、サンプルということでご容赦願いたい)。
なお、このVBのコードでは、Visual Basic 2005からの機能であるジェネリック型や、Visual Basic 2008から利用できるようになったオブジェクト初期化子、そしてVisual Basic 2010から利用できるようになった自動実装プロパティを使用している。

 次に、「EventDataForDisplay」クラスは、WPFの画面にバインドして表示するためのものだとする。画面には、次に挙げるプロパティを表示したいものとする。

EventTitle(イベントのタイトル)/EventDate(イベントの日付)/EventTime(イベントの開始時刻)/FormattedDateTime(イベント開催日時)

 また、データの変更を画面に反映させたいので、INotifyPropertyChangedインターフェース(System.ComponentModel名前空間)も実装する。すると、次のコードのようにちょっと複雑なクラスになる。

using System.Collections.Generic;
namespace dotNetTips1103VS2012
{
  public class EventDataForDisplay : System.ComponentModel.INotifyPropertyChanged 
  {
    // 公開するイベントハンドラー(INotifyPropertyChangedインターフェースの実装)
    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged(string info)
    {
      var handler = this.PropertyChanged;
      if (handler != null)
        handler(this, new System.ComponentModel.PropertyChangedEventArgs(info));
    }

    // 表示用のイベントタイトル(元データではTitleプロパティ)
    // 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
    private string _eventTitle;
    public string EventTitle
    {
      get { return _eventTitle; }
      set
      {
        if (string.Equals(_eventTitle, value))
          return;
        _eventTitle = value;
        NotifyPropertyChanged("EventTitle");
      }
    }

    // 表示用の日付(元データはDateTime型だが、これはString型)
    // 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
    private string _eventDate;
    public string EventDate
    {
      get { return _eventDate; }
      set
      {
        if (string.Equals(_eventDate, value))
          return;
        _eventDate = value;
        NotifyPropertyChanged("EventDate");
      }
    }

    // 表示用の時刻(元データはDateTime型だが、これはString型)
    // 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
    private string _eventTime;
    public string EventTime
    {
      get { return _eventTime; }
      set
      {
        if (string.Equals(_eventTime, value))
          return;
        _eventTime = value;
        NotifyPropertyChanged("EventTime");
      }
    }

    // 表示用の日時(元データにはGetFormattedDateTimeという名前のメソッドで実装されている)
    // 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
    private string _formattedDateTime;
    public string FormattedDateTime
    {
      get { return _formattedDateTime; }
      set
      {
        if (string.Equals(_formattedDateTime, value))
          return;
        _formattedDateTime = value;
        NotifyPropertyChanged("FormattedDateTime");
      }
    }
  }
}

Public Class EventDataForDisplay
  Implements ComponentModel.INotifyPropertyChanged

  ' 公開するイベントハンドラー(INotifyPropertyChangedインターフェースの実装)
  Public Event PropertyChanged(sender As Object, e As ComponentModel.PropertyChangedEventArgs) _
    Implements ComponentModel.INotifyPropertyChanged.PropertyChanged
  Private Sub NotifyPropertyChanged(info As String)
    RaiseEvent PropertyChanged(Me, _
                               New System.ComponentModel.PropertyChangedEventArgs(info))
  End Sub

  ' 表示用のイベントタイトル(元データではTitleプロパティ)
  ' 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
  Private _eventTitle As String
  Public Property EventTitle As String
    Get
      Return _eventTitle
    End Get
    Set(value As String)
      If (String.Equals(_eventTitle, value)) Then
        Return
      End If
      _eventTitle = value
      NotifyPropertyChanged("EventTitle")
    End Set
  End Property

  ' 表示用の日付(元データはDateTime型だが、これはString型)
  ' 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
  Private _eventDate As String
  Public Property EventDate As String
    Get
      Return _eventDate
    End Get
    Set(value As String)
      If (String.Equals(_eventDate, value)) Then
        Return
      End If
      _eventDate = value
      NotifyPropertyChanged("EventDate")
    End Set
  End Property

  ' 表示用の時刻(元データはDateTime型だが、これはString型)
  ' 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
  Private _eventTime As String
  Public Property EventTime As String
    Get
      Return _eventTime
    End Get
    Set(value As String)
      If (String.Equals(_eventTime, value)) Then
        Return
      End If
      _eventTime = value
      NotifyPropertyChanged("EventTime")
    End Set
  End Property

  ' 表示用の日時(元データにはGetFormattedDateTimeという名前のメソッドで実装されている)
  ' 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
  Private _formattedDateTime As String
  Public Property FormattedDateTime As String
    Get
      Return _formattedDateTime
    End Get
    Set(value As String)
      If (String.Equals(_formattedDateTime, value)) Then
        Return
      End If
      _formattedDateTime = value
      NotifyPropertyChanged("FormattedDateTime")
    End Set
  End Property
End Class

ロジックで使う「EventDataForDisplay」クラスの例(上:C#、下:VB)
先ほどのロジックで使う「EventData」クラスに比べて、かなり長いコードになっている。データの変更を画面に反映させるためにINotifyPropertyChangedインターフェースを実装したためだ。
このような複雑なクラスはロジックで使いたくない。また、ロジックと関係のないSystem.ComponentModel名前空間がロジックに入ってくるのも避けたいものだ。そのため、このように「EventData」と「EventDataForDisplay」の二つのクラスに分けて設計を進めることになる。

 また、画面には「EventDataForDisplay」オブジェクトのリストを表示したいとする。デザイン画面での利便性を考えると、「EventDataForDisplay」オブジェクトのコレクションを持つ「EventsList」クラスも必要になる(次のコード)。

public class EventsList
{
  private System.Collections.ObjectModel.ObservableCollection<EventDataForDisplay>
    _events = new System.Collections.ObjectModel.ObservableCollection<EventDataForDisplay>
                    (new List<EventDataForDisplay>());
  public System.Collections.ObjectModel.ObservableCollection<EventDataForDisplay>
    Events { get { return _events; } }

  public EventsList()
  {
    // デザイン時に表示するダミーデータを設定する
    // ……省略……
  }
}

Public Class EventsList
  Private _events _
    As System.Collections.ObjectModel.ObservableCollection(Of EventDataForDisplay) _
    = New System.Collections.ObjectModel.ObservableCollection(Of EventDataForDisplay) _
                      (New List(Of EventDataForDisplay)())
  Public ReadOnly Property Events _
      As System.Collections.ObjectModel.ObservableCollection(Of EventDataForDisplay)
    Get
      Return _events
    End Get
  End Property

  Public Sub New()
    ' デザイン時に表示するダミーデータを設定する
    ' ……省略……
  End Sub
End Class

画面にバインドするための「EventsList」クラスの例(上:C#、下:VB)
EventDataForDisplayオブジェクトを格納するObservableCollectionオブジェクト(System.Collections.ObjectModel名前空間)を、「Events」プロパティとして公開している。
WPFのXAMLコードの側でEventDataForDisplayオブジェクトのコレクションを生成してバインドするには、このようなクラスを用意しておくとよい。省略した部分でダミーデータを設定すれば、デザイン画面でもデータが表示されるようになる。
ちなみに、コードビハインドでObservableCollectionオブジェクトを生成して画面のコントロールにバインドするのであれば、このクラスは不要だ。しかしそうすると、デザイン画面ではデータが表示されない。
なお、このVBのコードでは、Visual Basic 2005からの機能であるジェネリック型を使用している。

 この「EventsList」クラスをWPFのListViewコントロール(System.Windows.Controls名前空間)にバインドして表示させた例を、次の画像に示す。

例題のクラスを使ってListViewにデータを表示した例(Visual Studio 2012) 例題のクラスを使ってListViewにデータを表示した例(Visual Studio 2012)
これはデバッグ実行しているところである。実際のコードは、別途公開のサンプルをご覧いただきたい。
実行画面とデザイン画面で表示が異なるのは、EventsListクラスのコンストラクターでデザイン時のダミーデータを設定しているからである(前出のコードでは省略している部分)。

AutoMapperで独自のマッピングを定義するには?

 AutoMapperのForMember拡張メソッドを使えばよい。

 「基本編」で説明したようにMapperクラス(AutoMapper名前空間)のCreateMapメソッドを呼び出したら、それに続けてForMember拡張メソッドを呼び出す。ForMember拡張メソッドの引数は二つで、次のようだ。

  • 第1引数:コピー先のプロパティ。文字列またはコピー先のオブジェクトからプロパティを取り出すラムダ式*1
  • 第2引数:コピー元からどのようにして値を取得するかというオプション指定。AutoMapperのMapFrom拡張メソッドを使ったラムダ式

 第2引数のラムダ式に使うMapFrom拡張メソッドは、一つのラムダ式を引数に取る。コピー元からコピーしたいデータを取り出すラムダ式を与える。

 このように説明するとなんだか複雑そうだが、次からの具体例を見てほしい。

*1 ラムダ式について詳しくは、次のMSDNのドキュメントを参照していただきたい。


異なるプロパティ名をマッピングするには?

 例えば、コピー元の「Title」プロパティの値をそのままコピー先の「EventTitle」プロパティにコピーする指定は、次のようなコードになる。

AutoMapper.Mapper.CreateMap<EventData, EventDataForDisplay>()
  .ForMember("EventTitle", opt => opt.MapFrom(src => src.Title));

AutoMapper.Mapper.CreateMap(Of EventData, EventDataForDisplay)() _
  .ForMember("EventTitle", Sub(opt) opt.MapFrom(Function(src) src.Title))

異なるプロパティ名をマッピングする例(上:C#、下:VB)
1行目は、「基本編」で示したマッピングを生成させるコードだ。
太字にした2行目が独自マッピングの定義である。ForMember拡張メソッドの第1引数にはコピー先のプロパティを指定する(ここでは文字列リテラルを使った)。第2引数のMapFrom拡張メソッド内には、コピー元からデータを取り出すラムダ式を与える。
なお、このVBのコードでは、Visual Basic 2008から利用できるようになったラムダ式を使用している。

データをコピーするときに型変換などを行わせるには?

 上のコードで、MapFrom拡張メソッド内にコピー元からデータを取り出すラムダ式を書いた。このラムダ式の返値がコピーされるのであるから、ここに型変換などの処理を書けばよい。例えば、コピー元のDateTime型を日付または時刻の文字列型に変換してコピーするには、次のようなコードになる。

AutoMapper.Mapper.CreateMap<EventData, EventDataForDisplay>()
  .ForMember("EventTitle", opt => opt.MapFrom(src => src.Title))
  .ForMember(dest => dest.EventDate,
              opt => opt.MapFrom(src => src.DateTime.ToString("M月d日")))
  .ForMember(dest => dest.EventTime,
              opt => opt.MapFrom(src => src.DateTime.ToString("H時m分")));

AutoMapper.Mapper.CreateMap(Of EventData, EventDataForDisplay)() _
  .ForMember("EventTitle", Sub(opt) opt.MapFrom(Function(src) src.Title)) _
  .ForMember(Function(dest) dest.EventDate,
              Sub(opt) opt.MapFrom(Function(src) src.DateTime.ToString("M月d日"))) _
  .ForMember(Function(dest) dest.EventTime,
              Sub(opt) opt.MapFrom(Function(src) src.DateTime.ToString("H時m分")))

データをコピーするときに型変換を行わせる例(上:C#、下:VB)
1行目/2行目は前のコードと同じ。
太字にした3行目/4行目が、コピー元のデータを加工してからコピーする独自マッピングの定義である。ForMember拡張メソッドの第1引数にはコピー先のプロパティを指定する(ここではラムダ式を使った)。第2引数のMapFrom拡張メソッド内に与えるラムダ式で、コピー元のデータを加工して返すようにしている(ここではDateTime型のToStringメソッドを呼び出している)。
なお、このVBのコードでは、Visual Basic 2008から利用できるようになったラムダ式と、Visual Basic 2010から利用できるようになった暗黙の行連結を使用している。

メソッドの返値をコピーさせるには?

 もうお分かりだと思うが、MapFrom拡張メソッドに与えるラムダ式でコピー元のメソッドを呼び出せばよい。例えば、次のコードのようにだ。

.ForMember(dest => dest.FormattedDateTime,
           opt => opt.MapFrom(src => src.GetFormattedDateTime()));

.ForMember(Function(dest) dest.FormattedDateTime,
           Sub(opt) opt.MapFrom(Function(src) src.GetFormattedDateTime()))

メソッドの返値をコピーさせる例(上:C#、下:VB)
ForMember拡張メソッドの第2引数のMapFrom拡張メソッド内に与えるラムダ式で、コピー元のGetFormattedDateTimeメソッドを呼び出して値を返すようにしている。
なお、このVBのコードでは、Visual Basic 2008から利用できるようになったラムダ式と、Visual Basic 2010から利用できるようになった暗黙の行連結を使用している。

 上のように書いてもよいのだが、実はAutoMapperは、コピー先と同じ名前のプロパティがコピー元にないときに、プロパティ名の先頭に「Get」を付けた名前のメソッドを自動的に探してくれるのである。すなわち、今回の例題では上のコードを記述しなくてもよいのだ。

まとめ

 以上をまとめて、例題の図に示したコピーを行うコードは次のようになる。

// ロジックを呼び出してデータを取得する
IList<EventData> data = EventData.GetData();

// 表示用のコレクションを用意する
var eventsList = new EventsList();

// AutoMapperを使ってデータを表示用のオブジェクトにコピーする
AutoMapper.Mapper.CreateMap<EventData, EventDataForDisplay>()
  .ForMember("EventTitle", opt => opt.MapFrom(src => src.Title))
  .ForMember(dest => dest.EventDate, 
             opt => opt.MapFrom(src => src.DateTime.ToString("M月d日")))
  .ForMember(dest => dest.EventTime, 
             opt => opt.MapFrom(src => src.DateTime.ToString("H時m分")));
foreach (EventData p in data)
  eventsList.Events.Add(
    AutoMapper.Mapper.Map<EventDataForDisplay>(p)
  );

// 表示用のコレクションをデータコンテキストにセット
this.DataContext = eventsList;

' ロジックを呼び出してデータを取得する
Dim data As IList(Of EventData) = EventData.GetData()

' 表示用のコレクションを用意する
Dim eventsList = New EventsList()

' AutoMapperを使ってデータを表示用のオブジェクトにコピーする
AutoMapper.Mapper.CreateMap(Of EventData, EventDataForDisplay)() _
  .ForMember("EventTitle", Sub(opt) opt.MapFrom(Function(src) src.Title)) _
  .ForMember(Function(dest) dest.EventDate,
             Sub(opt) opt.MapFrom(Function(src) src.DateTime.ToString("M月d日"))) _
  .ForMember(Function(dest) dest.EventTime,
             Sub(opt) opt.MapFrom(Function(src) src.DateTime.ToString("H時m分")))
For Each p As EventData In data
  eventsList.Events.Add(
    AutoMapper.Mapper.Map(Of EventDataForDisplay)(p)
  )
Next

' 表示用のコレクションをデータコンテキストにセット
Me.DataContext = eventsList

例題の図に示したコピーを行うコード(上:C#、下:VB)
これはWPFの画面のコードビハインドに記述する例である。ロジックを呼び出してデータを取得した後、それを表示用のコレクションに詰め替える処理を、AutoMapperを使って行っている。コードの全体は別途公開のサンプルをご覧いただきたい。
なお、このVBのコードでは、Visual Basic 2005からの機能であるジェネリック型や、Visual Basic 2008から利用できるようになったラムダ式と、Visual Basic 2010から利用できるようになった暗黙の行連結を使用している。

カテゴリ:オープンソース・ライブラリ 処理対象:データ型
カテゴリ:C# 処理対象:データ型
カテゴリ:Visual Basic 処理対象:データ型
関連TIPS:AutoMapperを使ってオブジェクト間のデータコピーを自動化するには?(基本編)


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。