AutoMapper で ReactiveProperty にマッピング
このカテゴリは WPF アプリで使用できるライブラリを紹介するエントリを分類するために新規で追加しました。このカテゴリに投稿するエントリは連載記事ではなく、今まで連載記事内に書いていたライブラリ紹介を連載とは別の単独記事にして参照し易くする事が目的です。第 1 回目に取り上げるのはオブジェクト間でメンバの値を自動コピーする時に便利な AutoMapper です。
尚、この記事は Visual Studio 2019 Community Edition で .NET Core 3.1 以上 と C# + Prism + ReactiveProperty + MahApps.Metro + Material Design In XAML Toolkit を使用して、WPF アプリケーションを MVVM パターンで作成するのが目的なので、C# の文法や基本的なコーディング知識を持っている人が対象です。
目次
AutoMapper とは
今回紹介する AutoMapper はオブジェクト間の値マッピングを自動化するためのライブラリで、同名のプロパティ同士をマッピングして値を移し替えるのが基本動作です。
AutoMapper も GitHub で開発を進めている OSS の 1 つで Nuget からインストールできます。
現時点(2020/9)で最新の AutoMapper Ver. 10.0.0 は .NET Standard 2.0 以降、.NET Framework 4.6.1 以降に対応しています。
AutoMapper の使い所
現在(2020/9)連載中の .NET Core WPF Prism MVVM 入門 2020 シリーズでも紹介している通り、WPF アプリを MVVM パターンで作成する場合、VM ⇔ View だけでなく Model ⇔ VM 間も双方向でバインドすると VM を薄く作る事ができ、Model へアプリケーションロジックを集中し易くなります。
ですが、Model を fig. 1 のような 2 階層で設計した場合、DataAccess 層は内部で生成したオブジェクトを返すような動作になる場合が多いと思います。
そのためアプリケーションロジック層では DataAccess 層で生成したクラスの値を VM 用のクラスへ移し替える処理が必要になります。まあ、実際は値を右から左に受け流すだけなので難しい訳ではありませんが、単純作業なので楽できるなら極力楽をしたい場合も多いと思います。
オブジェクト間で値の入れ替え
例えば src. 1 のような 2 つのクラスがあって PersonDto ⇒ PersonSlim へ値を移し替えたい場合があったとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | using System; using System.Reactive.Linq; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace PrismSample { /// <summary>DataAccess層で生成される個人情報を表します。</summary> public class PersonDto { /// <summary>IDを取得・設定します。</summary> public int? Id { get; set; } = null; /// <summary>個人名を取得・設定します。</summary> public string Name { get; set; } = string.Empty; /// <summary>個人名のフリガナを取得・設定します</summary> public string Kana { get; set; } = string.Empty; /// <summary>誕生日を取得・設定します</summary> public DateTime? BirthDay { get; set; } = null; } /// <summary>プロパティをReactivePropertySlimで定義したPerson。</summary> public class PersonSlim : DisposableModelBase { /// <summary>IDを取得・設定します。</summary> public ReactivePropertySlim<int?> Id { get; set; } /// <summary>名前を取得・設定します。</summary> public ReactivePropertySlim<string> Name { get; set; } /// <summary>誕生日を取得・設定します。</summary> public ReactivePropertySlim<DateTime?> BirthDay { get; set; } /// <summary>受け取ったPersonDtoの値に更新します。</summary> /// <param name="person">更新元のPersonDto。</param> public void UpdateFrom(PersonDto person) { this.Id.Value = person.Id; this.Name.Value = person.Name; this.BirthDay.Value = person.BirthDay; } /// <summary>コンストラクタ。</summary> public PersonSlim() : this(null, string.Empty, null) { } /// <summary>コンストラクタ。</summary> /// <param name="id">IDの初期値を表すint?</param> /// <param name="name">Nameの初期値を表す文字列。</param> /// <param name="birthDay">BirthDayの初期値を表すDateTime?。</param> public PersonSlim(int? id, string name, DateTime? birthDay) { this.Id = new ReactivePropertySlim<int?>(id) .AddTo(this.disposables); this.Name = new ReactivePropertySlim<string>(name) .AddTo(this.disposables); this.BirthDay = new ReactivePropertySlim<DateTime?>(birthDay) .AddTo(this.disposables); this.Age = this.calcAge.ToReadOnlyReactivePropertySlim() .AddTo(this.disposables); } } } |
管理人の場合は、38 ~ 43 行目の UpdateFrom のようなメソッドで値を入れ替えるように作成する事が多いと思います。まあ、移し替え対象のクラスが数種類、プロパティが 10 個程度なら src. 1 のように直接書いた方が早いと思いますが、プロパティが 50 ~ 100 個以上とか、対象クラスが数十種類にもなるとさすがに単純作業過ぎて嫌になると思います。
そんな場合に AutoMapper を使用すると Map メソッドを呼び出すだけで値の移し替えができるようになります。
AutoMapper のインストール
AutoMapper をインストールするには fig. 2 の『ソリューションの Nuget パッケージの管理 画面』で『automapper』を検索します。
fig. 2 では Model のアプリケーションロジック層に当たるプロジェクトにインストールしていますが、オブジェクトマッピングを使用する全プロジェクトへインストールする必要があります。
AutoMapper を使用する前準備
AutoMapper を使用すると Map メソッドだけで値の移し替えができると書きましたが、実際には前準備が必要でマッピングするクラスの組み合わせを予め登録しておく必要があります。AutoMapper は WPF 専用ではなく Windows Form や ASP.NET 等の Web アプリケーションでも使用でき、どのプラットフォームで使用する場合でも使用方法は変わりません。
マッピングするクラスの組み合わせを登録した AutoMapper の構成情報は 1 度作成すると後から追加・変更はできないのでアプリ起動時に 1 度だけ実行される箇所で作成します。ここでは Prism を使用した WPF アプリで使用する場合を例に紹介します。
Prism で AutoMapper を使用する
AutoMapper の構成情報は src. 2 のように作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; using AutoMapper; using Reactive.Bindings; namespace PrismSample { /// <summary>Mapperを作成します。</summary> public static class MapperFactory { /// <summary>Mapperを取得します。</summary> /// <returns>生成したIMapper。</returns> public static IMapper GetMapper() { var conf = new MapperConfiguration(c => { c.CreateMap<PersonDto, PersonDto>(); c.CreateMap<PersonDto, PlanePerson>(); }); conf.AssertConfigurationIsValid(); return conf.CreateMapper(); } } } |
AutoMapper は以下の手順で使用します。
- AutoMapper の構成情報へマッピングしたいクラスの組み合わせを登録する
※ 1 度作成するとアプリ実行中に追加・変更はできない - マッピング機能を呼び出すための IMapper を生成
- IMapper.Map でマッピング処理を実行する
まず、構成情報の作成は src. 2 のようにマッピングするクラスの組み合わせを MapperConfiguration に追加します。CreateMap メソッドの型パラメータは『マッピング元、マッピング先』の順に指定します。後は MapperConfiguration の CreateMapper から生成した IMapper のインスタンスを各所で使い回しつつオブジェクトマッピングを行います。
src. 2 は static な Factory として作成していますが、サンプルプロジェクトの都合と言うだけなので、static な Factory の作成は必須ではありません。
IMapper を Prism の DI コンテナに登録
Prism の場合、組み込みの DI コンテナを利用して AutoMapper をインジェクションできるので、DI コンテナへ登録するため Prism の Shell(スタートアップ)プロジェクトの App.xaml.cs へ src. 3 のハイライト行を追加します。
※ 当然ですが、src. 2 の MapperFactory を置いたプロジェクトへの参照設定が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | using System; using System.Reflection; using System.Windows; using Prism.Ioc; using Prism.Mvvm; namespace PrismSample { /// <summary>Interaction logic for App.xaml</summary> public partial class App { ~ 略 ~ /// <summary>DIコンテナにインジェクションするクラスを登録します。</summary> /// <param name="containerRegistry">登録用のDIコンテナを表すcontainerRegistry。</param> protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.Register<IPersonRepository, PersonRepository>(); containerRegistry.Register<IDataAgent, DataAgent>(); ~ 略 ~ containerRegistry.RegisterInstance(MapperFactory.GetMapper()); } } } |
DI コンテナに登録する IMapper はスレッドセーフなので同一インスタンスを使い回す事ができます。登録する IMapper は Singleton のように扱いたいので RegisterInstance で DI コンテナへ登録します。
ここでは MapperFactory を Model のアプリケーションロジック層に当たるプロジェクトに作成していますが、App.xaml.cs 内に記述しても構いません。要は MapperConfiguration と IMapper が作成できればどのような実装でも構いませんが、Prism を使用した MVVM パターンで作成する場合は DI コンテナを利用する前提で実装すべきだと思います。
Prism の DI コンテナについては .NET Core WPF Prism MVVM 入門 2020 step: 4 で詳しく紹介しているのでそちらを見てください。
IMapper を DI コンテナに登録すると src. 4 のように DI コンテナへ登録済みクラスのコンストラクタパラメータへ IMapper を追加するだけでインジェクションされます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; using System.Diagnostics; using System.Threading.Tasks; using AutoMapper; namespace PrismSample { /// <summary>データI/O用のAgentを表します。</summary> public class DataAgent : IDataAgent { ~ 略 ~ private IPersonRepository personRepository = null; private IMapper mapper = null; /// <summary>コンストラクタ。</summary> /// <param name="personRepo">DIコンテナからインジェクションされるIPersonRepository。</param> /// <param name="personRepo">DIコンテナからインジェクションされるIMapper。</param> public DataAgent(IPersonRepository personRepo, IMapper injectionMapper) { this.personRepository = personRepo; this.mapper = injectionMapper; } ~ 略 ~ } } |
以降は src. 4 の DataAgent へ AutoMapper 動作確認用の TestMapper メソッドを追加して、その中でマッピング処理を呼び出して動作を紹介していきます。
AutoMapper のマッピング処理に画面は関係ありませんが、.NET Core WPF Prism MVVM 入門 2020 シリーズで紹介中のサンプルを流用していて、参考までに fig. 3 のような画面から呼び出しています。
ついでに src. 4 で使用している PersonRepository は src. 5 のようなクラスです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using System; using System.IO; using System.Threading.Tasks; namespace PrismSample { /// <summary>Person用のリポジトリを表します。</summary> public class PersonRepository : IPersonRepository { ~ 略 ~ /// <summary>PersonDtoを取得します。</summary> /// <returns>取得したPersonDto。</returns> public PersonDto GetPersonDto() { return new PersonDto() { Id = 1, Name = "黒崎一護", BirthDay = new DateTime(1998, 7, 15) }; } ~ 略 ~ } } |
src. 5 では単純にクラスを new して返しているだけですが、本来は RDBMS や XAL ファイル等からデータを取得するクラスを想定しています。
AutoMapper でオブジェクトをマッピング
以降では AutoMapper を利用したオブジェクトマッピング例を紹介します。
同一クラスのマッピング
オブジェクトを複製したい場合、通常はシリアライズ等を利用してインスタンスを複製する事が多いと思いますが、src. 6 のように AutoMapper を利用する事もできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | using System; using System.Diagnostics; using AutoMapper; using Reactive.Bindings; namespace PrismSample { /// <summary>Mapperを作成します。</summary> public static class MapperFactory { /// <summary>Mapperを取得します。</summary> /// <returns>生成したIMapper。</returns> public static IMapper GetMapper() { var conf = new MapperConfiguration(c => { c.CreateMap<PersonDto, PersonDto>(); }); return conf.CreateMapper(); } } /// <summary>データI/O用のAgentを表します。</summary> public class DataAgent : IDataAgent { /// <summary>マッピングのテストを実行します。</summary> /// <param name="person">VMと双方向でバインドしているPersonSlim。</param> public void TestMapper(PersonSlim person) { var src = this.personRepository.GetPersonDto(); var dest = new PersonDto(); Debug.WriteLine("マッピング前src :" + src.ToString()); Debug.WriteLine("マッピング前dest :" + dest.ToString()); this.mapper.Map<PersonDto, PersonDto>(src, dest); Debug.WriteLine("マッピング後src :" + src.ToString()); Debug.WriteLine("マッピング後dest :" + dest.ToString()); } private IPersonRepository personRepository = null; private IMapper mapper = null; /// <summary>コンストラクタ。</summary> /// <param name="personRepo">DIコンテナからインジェクションされるIPersonRepository。</param> public DataAgent(IPersonRepository personRepo, IMapper injectionMapper) { this.personRepository = personRepo; this.mapper = injectionMapper; } } } |
AutoMapper でのオブジェクトマッピングは MapperConfiguration の設定が全てなので、以降のサンプルには前準備の設定部と実行部の両方を載せます。又、マッピング元・先の内容確認は Object.ToString をオーバーライドしてイミディエイトウィンドウに出力します。
src. 6 には DataAgent のコンストラクタや Debug.WriteLine を載せていますが、以降は省略します。IMapper をコンストラクタへインジェクションしたり、イミディエイトウィンドウへの出力処理はどのサンプルにも書いていると思って見てください。
console. 1 が実行結果です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | マッピング前src :クラス名: PersonDto Id: 1 Name: 黒崎一護 BirthDay: 1998/07/15 0:00:00 マッピング前dest :クラス名: PersonDto Id: Name: BirthDay: マッピング後src :クラス名: PersonDto Id: 1 Name: 黒崎一護 BirthDay: 1998/07/15 0:00:00 マッピング後dest :クラス名: PersonDto Id: 1 Name: 黒崎一護 BirthDay: 1998/07/15 0:00:00 |
console. 1 のように AutoMapper は同一プロパティ名同士をマッピングするのが基本動作です。シリアライズで複製すると新規インスタンスを new しますが、既存インスタンスに値を設定したい場合等には使えると思います。
別クラスへのマッピング
src. 7 は別クラスへのマッピング例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | using System; using System.Diagnostics; using AutoMapper; namespace PrismSample { /// <summary>一般的なPersonクラスを表します。</summary> public class PlanePerson { /// <summary>IDを取得・設定します。</summary> public int? Id { get; set; } = null; /// <summary>個人名を取得・設定します。</summary> public string Name { get; set; } = string.Empty; /// <summary>誕生日を取得・設定します</summary> public DateTime? BirthDay { get; set; } = null; } /// <summary>Mapperを作成します。</summary> public static class MapperFactory { /// <summary>Mapperを取得します。</summary> /// <returns>生成したIMapper。</returns> public static IMapper GetMapper() { var conf = new MapperConfiguration(c => { c.CreateMap<PersonDto, PersonDto>(); c.CreateMap<PersonDto, PlanePerson>(); }); conf.AssertConfigurationIsValid(); return conf.CreateMapper(); } } /// <summary>データI/O用のAgentを表します。</summary> public class DataAgent : IDataAgent { /// <summary>マッピングのテストを実行します。</summary> /// <param name="person">VMと双方向でバインドしているPersonSlim。</param> public void TestMapper(PersonSlim person) { var src = this.personRepository.GetPersonDto(); var dest = this.mapper.Map<PlanePerson>(src); } } } |
新規に追加した PlanePerson も PersonDto と構造はほとんど同じですが、PersonDto 側にある『Kana プロパティ』が PlanePerson 側には存在しません。AutoMapper では src. 7 のようにクラス同士の構造が一致していない場合でもマッピングできます。
又、『同一クラスのマッピング』で紹介したように既存のインスタンスへのマッピングもできますが、src. 7 のようにマッピング元だけを指定してマッピング先のインスタンスを生成する事も可能です。
console. 2 が実行結果です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | マッピング前src :クラス名: PersonDto Id: 1 Name: 黒崎一護 BirthDay: 1998/07/15 0:00:00 マッピング後src :クラス名: PersonDto Id: 1 Name: 黒崎一護 BirthDay: 1998/07/15 0:00:00 マッピング後dest :クラス名: PlanePerson Id: 1 Name: 黒崎一護 BirthDay: 1998/07/15 0:00:00 |
src. 7 のようにマッピング先にプロパティが存在しない場合は特に問題なくマッピングできますが、逆にマッピング先だけに存在するプロパティがあった場合はマッピングできず、console. 3 のような例外エラーメッセージがスタックトレースに吐かれます。
1 2 3 4 5 6 7 8 9 | Unmapped members were found. Review the types and members below. Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters ================================================= PersonDto -> PlanePerson (Destination member list) PrismSample.PersonDto -> PrismSample.PlanePerson (Destination member list) Unmapped properties: Sex |
console. 3 のメッセージの通り『Sex プロパティ』がマッピングできない事まで教えてくれます。console. 3 の例外はアプリ起動時に Throw される例外で、src. 7 32 行目のように『MapperConfiguration.AssertConfigurationIsValid メソッド』を呼び出すとマッピングの実行結果を事前に検証できます。
console. 3 のような例外が Throw された場合、AutoMapper の構成情報を src. 8 のように変更すると正常に実行できるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using System; using AutoMapper; using Reactive.Bindings; namespace PrismSample { /// <summary>Mapperを作成します。</summary> public static class MapperFactory { /// <summary>Mapperを取得します。</summary> /// <returns>生成したIMapper。</returns> public static IMapper GetMapper() { var conf = new MapperConfiguration(c => { c.CreateMap<PersonDto, PersonDto>(); c.CreateMap<PersonDto, PlanePerson>() .ForMember(d => d.Sex, o => o.Ignore()); }); conf.AssertConfigurationIsValid(); return conf.CreateMapper(); } } } |
src. 8 のように ForMember メソッドで『Sex プロパティ』を無視するように指定すれば正常にマッピングが行われます。src. 8 以外にも『AutoMapperを使用したオブジェクトのマッピング – やる気駆動型エンジニアの備忘録』で紹介されているように色々なマッピング手段が用意されています。
又『AutoMapperで特定の命名規則で変換する – xin9le.net』で紹介されているようにプロパティ名の命名規則を指定してマッピングする事もできます。
マッピング対象クラスは自分で作成しているので、プロパティ名を一致させる事は何とかなる場合も多いと思うので、ここではマッピング対象外のプロパティを指定する方法しか紹介しませんが、管理人的に重要なのはプロパティの型変換だと思っています。
このサイトで現在(2020/9 現在)連載中の .NET Core WPF Prism MVVM 入門 2020 シリーズでは Model と VM のクラスを双方向でバインドする方法を紹介していて Model のプロパティを ReactiveProperty で定義しています。そのため、このエントリの最大の目的は ReactiveProperty 型のプロパティを持つオブジェクトへのマッピングなので、次章では AutoMapper のカスタム型コンバータを紹介します。
AutoMapper のカスタム型コンバータ
AutoMapper のマッピング時型変換は大きく分けて以下の 2 通りの方法があります。
- コンバート処理をラムダ式で記述する
- AutoMapper に付属する ITypeConverter を継承したコンバータを作成する
と言う訳でとりあえずラムダ式のサンプルを書いてみようと試してみましたが、PersonDto.Id(元は int)を string に変更しても特に何も設定しないままマッピングできてしまいました… いろんなパターンを試した訳ではないのでどんな場合に型変換が必要なのかは分かりませんが、又何か分かればここに追記します。
そんな訳でラムダ式でコンバートする方法の紹介は止めて、本題の ReactiveProperty 型のプロパティにマッピングする方法を紹介します。
プロパティを ReactiveProperty で定義したオブジェクトへマッピング
マッピング先に src. 9 の PersonSlim のようなクラスを用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | using System; using AutoMapper; using Reactive.Bindings; namespace PrismSample { /// <summary>Mapperを作成します。</summary> public static class MapperFactory { /// <summary>Mapperを取得します。</summary> /// <returns>生成したIMapper。</returns> public static IMapper GetMapper() { var conf = new MapperConfiguration(c => { c.CreateMap<PersonDto, PersonSlim>(); c.CreateMap<PersonDto, PersonDto>(); c.CreateMap<PersonDto, Person>(); c.CreateMap<PersonDto, PlanePerson>() .ForMember(d => d.Sex, o => o.Ignore()); }); conf.AssertConfigurationIsValid(); return conf.CreateMapper(); } } /// <summary>プロパティをReactivePropertySlimで定義したPerson。</summary> public class PersonSlim : DisposableModelBase { /// <summary>IDを取得・設定します。</summary> public ReactivePropertySlim<int?> Id { get; set; } /// <summary>名前を取得・設定します。</summary> public ReactivePropertySlim<string> Name { get; set; } /// <summary>誕生日を取得・設定します。</summary> public ReactivePropertySlim<DateTime?> BirthDay { get; set; } /// <summary>年齢を取得します。</summary> public ReadOnlyReactivePropertySlim<int> Age { get; } private ReactivePropertySlim<int> calcAge { get; set; } /// <summary>コンストラクタ。</summary> public PersonSlim() : this(null, string.Empty, null) { } /// <summary>コンストラクタ。</summary> /// <param name="id">IDの初期値を表すint?</param> /// <param name="name">Nameの初期値を表す文字列。</param> /// <param name="birthDay">BirthDayの初期値を表すDateTime?。</param> public PersonSlim(int? id, string name, DateTime? birthDay) { this.calcAge = new ReactivePropertySlim<int>(0) .AddTo(this.disposables); this.Id = new ReactivePropertySlim<int?>(id) .AddTo(this.disposables); this.Name = new ReactivePropertySlim<string>(name) .AddTo(this.disposables); this.BirthDay = new ReactivePropertySlim<DateTime?>(birthDay) .AddTo(this.disposables); this.Age = this.calcAge.ToReadOnlyReactivePropertySlim() .AddTo(this.disposables); this.BirthDay.Subscribe(d => { if (d.HasValue) this.calcAge.Value = DateTime.Now.Year - d.Value.Year; }); } } /// <summary>データI/O用のAgentを表します。</summary> public class DataAgent : IDataAgent { /// <summary>マッピングのテストを実行します。</summary> /// <param name="person">VMと双方向でバインドしているPersonSlim。</param> public void TestMapper(PersonSlim person) { var src = this.personRepository.GetPersonDto(); this.mapper.Map<PersonDto, PersonSlim>(src, person); } } } |
ReactiveProperty については .NET Core WPF ReactiveProperty 入門 2020 step: 7 で詳しく紹介しているのでそちらを見てください。
MapperConfiguration へ単純に PersonDto とのマッピングセットを追加しただけだと console. 4 のような例外メッセージが出力されます。
1 2 3 4 5 6 | The following member on PrismSample.PersonSlim cannot be mapped: Id Add a custom mapping expression, ignore, add a custom resolver, or modify the destination type PrismSample.PersonSlim. Context: Mapping to member Id from PrismSample.PersonDto to PrismSample.PersonSlim Exception of type 'AutoMapper.AutoMapperConfigurationException' was thrown. |
console. 4 の例外はマッピング先のプロパティをどのように扱えばいいか AutoMapper が判断できない場合に Throw される例外のようです。
ReactiveProperty 型のプロパティにマッピングしたい場合は src. 10 のような型コンバータを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | using System; using AutoMapper; using Reactive.Bindings; namespace PrismSample { /// <summary>Mapperを作成します。</summary> public static class MapperFactory { /// <summary>Mapperを取得します。</summary> /// <returns>生成したIMapper。</returns> public static IMapper GetMapper() { var conf = new MapperConfiguration(c => { c.CreateMap<int?, ReactivePropertySlim<int?>>().ConvertUsing<ReactivePropertySlimConverter<int?>>(); c.CreateMap<string, ReactivePropertySlim<string>>().ConvertUsing<ReactivePropertySlimConverter<string>>(); c.CreateMap<DateTime?, ReactivePropertySlim<DateTime?>>().ConvertUsing<ReactivePropertySlimConverter<DateTime?>>(); c.CreateMap<PersonDto, PersonSlim>(); c.CreateMap<PersonDto, PersonDto>(); c.CreateMap<PersonDto, PlanePerson>() .ForMember(d => d.Sex, o => o.Ignore()); }); conf.AssertConfigurationIsValid(); return conf.CreateMapper(); } } /// <summary>TをReactivePropertySlim<T>に変換します。</summary> /// <typeparam name="T">ReactivePropertySlimに指定する型パラメータと同じ型を表します。</typeparam> class ReactivePropertySlimConverter<T> : ITypeConverter<T, ReactivePropertySlim<T>> { public ReactivePropertySlim<T> Convert(T source, ReactivePropertySlim<T> destination, ResolutionContext context) { destination.Value = source; return destination; } } /// <summary>データI/O用のAgentを表します。</summary> public class DataAgent : IDataAgent { /// <summary>マッピングのテストを実行します。</summary> /// <param name="person">VMと双方向でバインドしているPersonSlim。</param> public void TestMapper(PersonSlim person) { var src = this.personRepository.GetPersonDto(); this.mapper.Map<PersonDto, PersonSlim>(src, person); } } } |
マッピング元の型とマッピング先 ReactivePropertySlim の型パラメータが同じ場合は、src. 10 のように Generic のコンバータクラスを 1 つ用意すれば済みます。但し、おそらくは『int と Slim<int?>』等のように型が違う場合は組み合わせ毎にコンバータを作成する必要があると思います。加えて src. 10 は ReactivePropertySlim に対するコンバータですが、ReactiveProperty 型のプロパティにマッピングしたい場合にも別途コンバータを作成する必要があります。
Generic なコンバータにすれば 1 つ作成するだけで済みますが、MapperConfiguration へ追加する際は src. 10 のように型毎の指定が必要です。MapperConfiguration に CreateMap するには順番があってコンバータはクラスの組み合わせより前に記述しないと console. 4 と同じ例外が Throw されます。
そして ReactiveProperty(Slim)用コンバータ内では .NET Core WPF Prism MVVM 入門 2020 step: 7 でも紹介している通り、値は必ず【.Value】に設定しなければならない点も重要です。
又、ReactiveProperty(Slim)型のプロパティは『{ get; set; }』で定義しないとマッピングできません。通常 ReactiveProperty(Slim)でプロパティを定義する場合は『{ get; }』のみの読み取り専用で定義しますが、AutoMapper でマッピングするプロパティには Setter も必要です。
AutoMapper が提供するカスタム型コンバータ用の ITypeConverter は 1 種類のみで値を返すメソッドしか定義されていないので ReactiveProperty(Slim)型のプロパティを『{ get; set; }』で定義しなければならないのは残念ですが、src. 10 を実行すると console. 5 のように正常にマッピングできる事が確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | マッピング前src :クラス名: PersonDto Id: 1 Name: 黒崎一護 BirthDay: 1998/07/15 0:00:00 マッピング前person :クラス名: PersonSlim Id: Name: BirthDay: マッピング後src :クラス名: PersonDto Id: 1 Name: 黒崎一護 BirthDay: 1998/07/15 0:00:00 マッピング後person :クラス名: PersonSlim Id: 1 Name: 黒崎一護 BirthDay: 1998/07/15 0:00:00 |
src. 10 のように型コンバータを使用すれば ReactiveProperty のようなクラスを型に指定したプロパティにもマッピングできるので、DDD(Domain-driven design:ドメイン駆動設計)等でもお馴染みの ValueObject を使用したクラスにもマッピングできると思います。
ReactiveProperty 型コンバートの検証
カスタム型コンバータで値のマッピングが正常動作している事は確認できましたが、マッピング先の型が ReactiveProperty(Slim)である点が少し気掛りです。通常、ReactiveProperty(Slim)はコンストラクタで CompositeDisposable へ追加したり、Subscribe で値の変更監視を設定する場合も多いと思います。
src. 10 の通りコンバータは戻り値を返しているので、マッピングした後でも Subscribe や Dispose が動作しないと意味がありません。まあ、パラメータで受け取ったインスタンスをそのまま返しているので大丈夫だとは思いますが念のため確認しておきます。
Subscribe の動作確認
まず、fig. 4 の画面で Subscribe の動作を確認します。
fig. 4 は誕生日を Subscribe して現在日時から年齢を算出しているので、マッピング実行後でも年齢が更新されれば Subscribe はちゃんと動作している事になります。
fig. 4 の通りマッピング実行後も Subscribe は問題無く動作しているようです。
参照先を比較して検証
念のため参照先を比較して検証します。PersonSlim のコンストラクタで ReactiveProperty の参照を別フィールドに退避しておいて、マッピング後に ReferenceEquals で参照先を比較する処理を追加しました。ここに紹介する程の内容では無いのでソースコードは GitHub で確認してください。
console. 6 が比較結果です。
1 2 3 | ID: True Name: True BirthDay: True |
インスタンス作成直後とマッピング処理後の参照先が同一である事が確認できます。
Dispose の確認
念には念を入れてちゃんと Dispose されるかも確認しました。PersonSlim.Dispose を override して Dispose されるかを確認しました。前項と同じくソースコードは GitHub で確認してください。
console. 7 が実行結果です。
1 2 3 4 5 6 7 | Dispose前 Id: False Dispose前 Name: False Dispose前 BirthDay: False Dispose後 Id: True Dispose後 Name: True Dispose後 BirthDay: True |
ReactiveProperty(Slim).IsDisposed は Dispose を呼び出すと true にセットされるプロパティなので CompositeDisposable.Dispose の呼び出しもちゃんと行われているようです。
ReactiveProperty の Dispose についても .NET Core WPF ReactiveProperty 入門 2020 step: 7 で紹介しているので詳しくはそちらを見てください。
ここまでの検証結果から ReactiveProperty 型のプロパティを AutoMapper のカスタム型コンバータ経由でマッピングしても問題は無いと考えて良いと思います。
逆マッピング
.NET Core WPF ReactiveProperty 入門 2020 step: 7 でも紹介しましたが、現時点(2020/9)、ReactiveProperty 型のプロパティを含むクラスはシリアライズ・デシリアライズする方法がありません。そのため上で紹介している PersonSlim クラスを XML へ書き出す方法はありません。
ですが、ReactiveProperty 型を使用しないクラスであれば当然シリアライズもデシリアライズもできます。例えば上のマッピングサンプルで紹介した PersonDto ⇒ PersonSlim のマッピングを利用すれば回避できます。PersonDto ⇒ PersonSlim はデシリアライズ時に実行するので、シリアライズ時には逆方向のマッピングが必要になるのは分かると思います。
マッピング元 ⇒ マッピング先の一方向だけでなく双方向にマッピングしたい場合は src. 11 のような構成情報を作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | using System; using AutoMapper; using Reactive.Bindings; namespace PrismSample { /// <summary>Mapperを作成します。</summary> public static class MapperFactory { /// <summary>Mapperを取得します。</summary> /// <returns>生成したIMapper。</returns> public static IMapper GetMapper() { var conf = new MapperConfiguration(c => { c.CreateMap<int?, ReactivePropertySlim<int?>>().ConvertUsing<ReactivePropertySlimConverter<int?>>(); c.CreateMap<ReactivePropertySlim<int?>, int?>().ConvertUsing<ToReactivePropertySlimConverter<int?>>(); c.CreateMap<string, ReactivePropertySlim<string>>().ConvertUsing<ReactivePropertySlimConverter<string>>(); c.CreateMap<ReactivePropertySlim<string>, string>().ConvertUsing<ToReactivePropertySlimConverter<string>>(); c.CreateMap<DateTime?, ReactivePropertySlim<DateTime?>>().ConvertUsing<ReactivePropertySlimConverter<DateTime?>>(); c.CreateMap<ReactivePropertySlim<DateTime?>, DateTime?>().ConvertUsing<ToReactivePropertySlimConverter<DateTime?>>(); c.CreateMap<PersonDto, PersonSlim>() .ReverseMap(); ~ 略 ~ }); conf.AssertConfigurationIsValid(); return conf.CreateMapper(); } } /// <summary>TをReactivePropertySlim<T>に変換します。</summary> /// <typeparam name="T">ReactivePropertySlimに指定する型パラメータと同じ型を表します。</typeparam> class ReactivePropertySlimConverter<T> : ITypeConverter<T, ReactivePropertySlim<T>> { public ReactivePropertySlim<T> Convert(T source, ReactivePropertySlim<T> destination, ResolutionContext context) { destination.Value = source; return destination; } } /// <summary>ReactivePropertySlim<T>をTに変換します。</summary> /// <typeparam name="T">ReactivePropertySlimに指定する型パラメータと同じ型を表します。</typeparam> class ToReactivePropertySlimConverter<T> : ITypeConverter<ReactivePropertySlim<T>, T> { public T Convert(ReactivePropertySlim<T> source, T destination, ResolutionContext context) => source.Value; } /// <summary>データI/O用のAgentを表します。</summary> public class DataAgent : IDataAgent { ~ 略 ~ /// <summary>マッピングのテストを実行します。</summary> /// <param name="person">VMと双方向でバインドしているPersonSlim。</param> public void TestMapper(PersonSlim person) { var src = this.personRepository.GetPersonDto(); this.mapper.Map<PersonDto, PersonSlim>(src, person); var revMap = new PersonDto(); this.mapper.Map<PersonSlim, PersonDto>(person, revMap); } ~ 略 ~ } } |
双方向にオブジェクトマッピングする場合は src. 11 のように ReverseMap を呼び出すだけですが、ReactiveProperty 型のプロパティを含むクラスが対象の場合は、逆方向のコンバータも作成して構成情報に追加しないとマッピング実行時に例外が Throw されます。
逆マッピング用のコンバータは src. 11 の 45 ~ 49 行目のように ReactiveProperty.Value を返すだけの単純な Generic クラスを作成して MapperConfiguration に追加すれば逆マッピングも正常に動作するので、ReactivePropertySlim を XML ファイル等にシリアライズしたい場合の回避策にも使えます。
まとめ的な
AutoMapper は必須ライブラリではなく『あると便利な省力化推進系ユーティリティ』に分類されるので、業界的に導入が難しい場合も多いと思いますが、使用するクラスが多ければ多い程導入した場合の効果も大きくなると思います。とは言え、複雑な構造のクラスは AutoMapper を使用するより値の入れ替え処理を直接書いた方が早い(効率・動作速度の両方)場合もあるので、単純構造のクラスが多数あるような場合は効果があると思います。
加えてデータアクセス層で Dapper 等の ORM を使用する場合、WPF MVVM L@bo #5 では Dapper から ReactivePropertySlim 型のプロパティを含むクラスに直接マッピングする方法を紹介しましたが、AutoMapper を使用すれば、Dapper から ReactivePropertySlim 型のプロパティを含むクラスに無理矢理マッピングする必要も無くなります。
Dapper では POCO なクラスにマッピングして、アプリケーションロジック層等で ReactivePropertySlim 型のプロパティを含むクラスに AutoMapper を使って値を移し替える方がシンプルで良いと思います。
WPF アプリを MVVM パターンで作成する場合はオブジェクト間で値を入れ替えたい場合も多いと思うので、導入できそうなら導入した方が開発効率もそれなりに上がると思います。
ここで紹介したサンプルコードはいつものように GitHub リポジトリ に上げていますが、現在連載中の .NET Core WPF Prism MVVM 入門 2020 シリーズのサンプルに相乗りして作成したので、【07_Step08 のソリューション】を見てください。