AutoMapperを使って異なるオブジェクト間のデータコピーを自動化するには?(独自マッピング編):.NET TIPS
AutoMapperを使って、名前の異なるプロパティ/型が異なるプロパティなどの間でのデータコピーを自動化する方法を解説する。
対象:.NET 4以降
オープンソースのライブラリ「AutoMapper」を使うと、異なるクラス間のデータコピーを自動化できる。「基本編」ではプロパティ名が同じ場合を紹介したが、プロパティ名が異なる場合はどうなのだろうか? 独自のマッピングを定義することで、その場合もデータコピーを自動化できる。さらには、データをコピーする際に型変換などの処理を実行させることもできるのだ。本稿では、そのようなAutoMapperのマッピング機能の使い方を紹介する。
なお、本稿執筆時点でAutoMapperがサポートしているのは.NET Framework 4以降であるが、NuGetから導入するため、本稿ではVisual Studio 2012を使って説明する。また、本稿のサンプルは「MSDN Code Recipe:.NET Tips #1103」からダウンロードできる。
AutoMapperを導入するには?
「.NET TIPS:AutoMapperを使ってオブジェクト間のデータコピーを自動化するには?(基本編)」をご覧いただきたい。
例題
ここでは例題として、催事の情報を格納する「EventData」クラスと「EventDataForDisplay」クラスという二つのクラスを考えてみよう。この二つのクラスのオブジェクト間で、次の図のようなデータコピーを行いたいものとする。
- 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
「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
先ほどのロジックで使う「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
EventDataForDisplayオブジェクトを格納するObservableCollectionオブジェクト(System.Collections.ObjectModel名前空間)を、「Events」プロパティとして公開している。
WPFのXAMLコードの側でEventDataForDisplayオブジェクトのコレクションを生成してバインドするには、このようなクラスを用意しておくとよい。省略した部分でダミーデータを設定すれば、デザイン画面でもデータが表示されるようになる。
ちなみに、コードビハインドでObservableCollectionオブジェクトを生成して画面のコントロールにバインドするのであれば、このクラスは不要だ。しかしそうすると、デザイン画面ではデータが表示されない。
なお、このVBのコードでは、Visual Basic 2005からの機能であるジェネリック型を使用している。
この「EventsList」クラスをWPFのListViewコントロール(System.Windows.Controls名前空間)にバインドして表示させた例を、次の画像に示す。
例題のクラスを使ってListViewにデータを表示した例(Visual Studio 2012)
これはデバッグ実行しているところである。実際のコードは、別途公開のサンプルをご覧いただきたい。
実行画面とデザイン画面で表示が異なるのは、EventsListクラスのコンストラクターでデザイン時のダミーデータを設定しているからである(前出のコードでは省略している部分)。
AutoMapperで独自のマッピングを定義するには?
AutoMapperのForMember拡張メソッドを使えばよい。
「基本編」で説明したようにMapperクラス(AutoMapper名前空間)のCreateMapメソッドを呼び出したら、それに続けてForMember拡張メソッドを呼び出す。ForMember拡張メソッドの引数は二つで、次のようだ。
- 第1引数:コピー先のプロパティ。文字列またはコピー先のオブジェクトからプロパティを取り出すラムダ式*1
- 第2引数:コピー元からどのようにして値を取得するかというオプション指定。AutoMapperのMapFrom拡張メソッドを使ったラムダ式
第2引数のラムダ式に使うMapFrom拡張メソッドは、一つのラムダ式を引数に取る。コピー元からコピーしたいデータを取り出すラムダ式を与える。
このように説明するとなんだか複雑そうだが、次からの具体例を見てほしい。
異なるプロパティ名をマッピングするには?
例えば、コピー元の「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))
データをコピーするときに型変換などを行わせるには?
上のコードで、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分")))
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()))
上のように書いてもよいのだが、実は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# 処理対象:データ型
カテゴリ:Visual Basic 処理対象:データ型
関連TIPS:AutoMapperを使ってオブジェクト間のデータコピーを自動化するには?(基本編)
Copyright© Digital Advantage Corp. All Rights Reserved.