やりたいこと
こんな感じの仕組みを作りたい。
こういうのが出来たら、変更できる部分と変更できない部分が明確に変えられるので、インタフェースの線引きをするのに重宝しそうです。
AutoMapperについて
Qiitaでは連載記事10件以下とマイナーなAutoMapperですが、世界を見渡すとよく使われていて、定期的にアップデートされています。
今後、.NET ASP Core 6/8へのLTS移行に向けてもメンテナンスは長く行われるでしょう。
Windows固有コードはないはずなので、Mac, Linuxでも可能なはず。
そう考えると、Visual StudioをIDEとして使う環境としては、(現在も十分広まっていますが)今後より活躍するライブラリだと思います
不人気な理由としては、ASP.NET専用のライブラリだと思われているからというのが大きいと思います。
確かに、MicrosoftとしてはASP .NETに向けて開発する方向性を示していますが、これ以外の環境でも十分に活躍できるライブラリだと思います。
でも、AutoMapperなしだと、正直超めんどくさい
例えば、こんなクラスがあったとして・・・
struct SampleStructIn
{
/* ID */
public int Id
{
get;
set;
}
/* アイスの味の名前 */
public string FlavorName
{
get;
set;
}
/* 購入日 */
public DateTime Date
{
get;
set;
}
}
class SampleClass
{
int _id;
string _flavor_name;
/* アイスの味の名前 */
public string FlavorName
{
get {
return _flavor_name;
}
}
/* 購入日 */
public DateTime Date
{
get;
set;
}
/* 購入日の曜日 */
public DateTime DayOfWeek {
get;
set;
}
}
struct SampleStructOut
{
/* ID */
public int Id
{
get;
set;
}
/* アイスの味の名前 */
public string FlavorName
{
get;
set;
}
/* 購入日 */
public DateTime Date
{
get;
set;
}
public string DayOfWeek {
get;
set;
}
}
と新たな中間クラスと出力クラスを定義して、
public SampleStructOut Update(SampleStructIn sample_obj) {
/* 型変換 */
var internal_sample_obj = new SampleClass() {
FlavorName = sample_obj.FlavorName,
Date = sample_obj.Date
};
/***/
/* ここで、internal_sample用の処理 */
/***/
UpdateInternal(internal_sample);
/* 更新:【こんな書き方は面倒すぎてしたくねえ!!!!!】 */
var sample_out = new SampleStructOut() {
Id = sample_obj.Id,
FlavorName = sample_obj.FlavorName,
Date = internal_sample_obj.Date,
DayOfWeek = internal_sample_obj.DayOfWeek
};
return sample_out;
}
と書くわけですが、こんなん書いてたらコード量も増えるし、面倒すぎる!
sample_objとinternal_sample_objがごっちゃになるのも美しくありません。
こんなことするくらいなら、SampleStructOutをstructではなく、classにして定義して、IdとFlavorの両方を消して、SampleClassも使わずに、
public SampleStructOut Update(SampleStructIn sample_obj) {
/* 実際はメソッドUpdateでは、こういう書き方になると思うが、*/
sample_out = UpdateInternal(sample_obj);
/* メソッドUpdateIntervalの中身にはこういう定義が必ず発生する */
var sample_out = new SampleStructOut() {
Date = date,
DayOfWeek = dayofweek
};
return sample_out;
}
として素直に書きますよねw けれど、こうしてしまうと、DateはInとOutでダブるし、SampleStructInとSampleStructOutを両方管理しないといけないしで、Update以外のメソッドが美しくないコードになるわけです。
この課題をAutoMapperで解決します。
AutoMapperをインストール(nuget)
nugetマネージャーコンソールで
install-package automapper
と入力してダウンロードします。
クラス図
こんな感じでクラスを構成しておきます。ポイントは、BaseClassのところで、Tに変換前のクラスを定義することがポイントです。このSourceには、SampleStructInの内容が入るので、この部分でgetterのみという定義が可能になります。
クラス定義
struct SampleStructIn
{
public int Id
{
get;
set;
}
public string FlavorName
{
get;
set;
}
public DateTime Date
{
get;
set;
}
}
struct SampleStructOut
{
public int Id
{
get;
set;
}
public string FlavorName
{
get;
set;
}
public DateTime Date
{
get;
set;
}
public string DayOfWeek
{
get;
set;
}
}
中間クラスSampleClassは次のように定義します。
class BaseClass<T>
where T : struct
{
private T _instance;
private bool _is_lock;
public T Source
{
get
{
return _instance;
}
set
{
if (_is_lock)
{
throw new InvalidOperationException("Source was locked");
}
else
{
_instance = value;
}
}
}
public void LockSource()
{
_is_lock = true;
}
}
class SampleClass : BaseClass<SampleStructIn>
{
/* Id, FlavorNameはSourceのgetterになっているのがポイント! */
public int Id
{
get
{
return Source.Id;
}
}
public string FlavorName
{
get
{
return Source.FlavorName;
}
}
public DateTime Date
{
get;
set;
}
public string DayOfWeek
{
get;
set;
}
}
こうしてしまえば、IdやFlavorNameはgetterとして定義され、内部メソッドに渡しても変更される危険性が低くなります。低くなります、と言っているのは、クラス内部からは一応Sourceとして変えられてしまうので、メソッドを定義した場合はSource変更に対してSafeな処理に必要があります。
ProfileでAutoMapperにクラスの関係性を登録
using AutoMapper;
class UserProfile : Profile
{
public UserProfile()
{
CreateMap<SampleStructIn, SampleClass>()
.ForMember(m => m.Source, option => {
option.MapFrom(source => source);
})
.AfterMap((source, target) => { target.LockSource(); });
CreateMap<SampleClass, SampleStructOut>();
}
}
Profileというクラスを継承して、新たにコンストラクタを定義します。この中に、CreateMapを定義して、変換前と変換後の関係を定義します。
途中にForMember、AfterMapという表記がありまして、これがSourceのgetterを完全にやるためのポイントです。
-
ForMemberと書くことで、変換後にSampleStructInをSampleClassに代入しています。
-
AfterMapと書くことで、型変換の処理を終了したときの処理を書いていて、それ以降にSourceを設定しようとしたらInvalidOperationExceptionを出力するようにしています。したがって、外部からはSourceを書き換えることが出来ません。 SampleStructInが値クラスなので、getterを使って操作することも出来ない状態です。
実行コード
とりあえずコンソールアプリで、次のように書きます
コンソールアプリで実行して可能なので、automapperさえあればどのようなC#の開発環境でも実行できるのがポイントですね。
class Program
{
static void Main(string[] args)
{
/* AutoMapperの初期設定 */
/* 先ほど定義したUserProfileを定義します */
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new UserProfile());
});
IMapper mapper = mapperConfig.CreateMapper();
/* IMapper型のインタフェースmapperを介して変換を行います */
/* 実際に使うときは、このオブジェクトをシングルトンで持たせる運用が良いでしょう */
var struct_in = new SampleStructIn()
{
Id = 3,
FlavorName = "Vanilla",
Date = DateTime.Now
};
/* 値クラスからクラスに変換 */
var class_instance = mapper.Map<SampleClass>(struct_in);
/* SampleModelというクラスでUpdateの処理をする*/
var model_update = new SampleModel(class_instance);
model_update.Update();
/* クラスから値クラスに変換 */
var struct_out = mapper.Map<SampleStructOut>(class_instance);
Console.WriteLine(struct_out.FlavorName);
Console.WriteLine(struct_out.DayOfWeek);
}
}
class SampleModel
{
private SampleClass _input;
public SampleModel(SampleClass input)
{
_input = input;
}
public void Update()
{
_input.DayOfWeek = _input.Date.DayOfWeek.ToString();
}
}
出力結果:
Vanilla
Saturday
上記のようなコードにすれば、struct_inに対して、struct_outは「DayOfWeekだけが増えている」ものとして受け入れることが出来ます。これは非常に管理しやすいもので、「メソッドを実行した前」「メソッドを実行した後」というオブジェクトになっていますので、
上位の最終的な処理はstruct_outのみを使って管理する、みたいなことが出来るようになります。
Listとの連携
LINQ(List)とも簡単に連携できます。
そういう意味では、きちっと型定義してくれるのってすごくいいなと思いました。
(Dictionaryでも試したいけど、今回はやっていません)
クラス定義
using System.Collections.Generic;
struct SampleStructListIn
{
public int Id
{
get;
set;
}
public string UserName
{
get;
set;
}
public IEnumerable<SampleStructIn> Items
{
get;
set;
}
}
using System.Collections.Generic;
struct SampleStructListOut
{
public int Id
{
get;
set;
}
public string UserName
{
get;
set;
}
public int NumberOfResponse
{
get;
set;
}
public IEnumerable<SampleClass> Items
{
get;
set;
}
}
IEnumerableで書いてますが、ListでもArrayListでも構いません。IEnumerableで書く最大の特徴は、入力側からの変更が発生しないことですね。
Outで追加される値
-
NumberOfResponseが追加される
-
ItemsのDayOfWeekが追加される
-
ItemsのDateが変更される
このくらいになってくると、実際の開発でも想定される話になると思います。LINQでのジェネリクス内のクラスの内部の変数を変更したいというのは普通にあると思うので。
中間クラス
using System.Collections.Generic;
class SampleClassList : BaseClass<SampleStructListIn>
{
public int Id
{
get
{
return Source.Id;
}
}
public string UserName
{
get
{
return Source.UserName;
}
}
public int NumberOfResponse
{
get;
set;
}
public List<SampleClass> Items
{
get;
set;
}
}
UserProfile
using AutoMapper;
class UserProfile : Profile
{
public UserProfile()
{
CreateMap<SampleStructIn, SampleClass>()
.ForMember(m => m.Source, option => {
option.MapFrom(source => source);
})
.AfterMap((source, target) => { target.LockSource(); });
CreateMap<SampleClass, SampleStructOut>();
CreateMap<SampleStructListIn, SampleClassList>()
.ForMember(m => m.Source, option =>
{
option.MapFrom(source => source);
})
.AfterMap((source, target) => { target.LockSource(); }); ;
CreateMap<SampleClassList, SampleStructListOut>();
}
}
実行コード
class Program
{
static void Main(string[] args)
{
/* AutoMapperの初期設定 */
/* 先ほど定義したUserProfileを定義します */
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new UserProfile());
});
/* IMapper型のインタフェースmapperを介して変換を行います */
/* 実際に使うときは、このオブジェクトをシングルトンで持たせる運用が良いでしょう */
IMapper mapper = mapperConfig.CreateMapper();
/* Listを適当に定義する */
var struct_list_in = new SampleStructListIn()
{
Id = 55,
UserName = "Hanako",
Items = new List<SampleStructIn>()
{
new SampleStructIn()
{
Id = 4,
FlavorName = "Chocolate",
Date = DateTime.Now.AddDays(-2),
},
new SampleStructIn()
{
Id = 5,
FlavorName = "Strawberry",
Date = DateTime.Now.AddDays(-1)
},
new SampleStructIn()
{
Id = 8,
FlavorName = "Hawaiian Blue",
Date = DateTime.Now.AddDays(0)
}
}
};
/* 値クラスからクラスに変換 */
var class_insntance_list = mapper.Map<SampleClassList>(struct_list_in);
/* SampleModelListというクラスでUpdateの処理をする*/
var model_update_list = new SampleModelList(class_insntance_list);
model_update_list.Update();
/* クラスから値クラスに変換 */
var struct_out_list = mapper.Map<SampleStructListOut>(class_insntance_list);
/* 結果出力 */
Console.WriteLine("User FlavorName:{0}", struct_out_list.UserName);
Console.WriteLine("Number:{0}", struct_out_list.NumberOfResponse);
foreach (var item in struct_out_list.Items)
{
Console.WriteLine("{0},{1},{2}", item.FlavorName, item.Date.ToString(), item.DayOfWeek);
}
}
}
/* 処理を実行するためのクラス */
class SampleModelList
{
private SampleClassList _input;
public SampleModelList(SampleClassList input)
{
_input = input;
}
public void Update()
{
/* Items数の代入処理と、DayOfWeekへの代入を行う */
_input.NumberOfResponse = _input.Items.Count;
foreach (var item in _input.Items)
{
item.DayOfWeek = item.Date.DayOfWeek.ToString();
}
}
}
実行結果
User FlavorName:Hanako
Number:3
Chocolate,5/20/2021 7:29:46 PM,Thursday
Strawberry,5/21/2021 7:29:46 PM,Friday
Hawaiian Blue,5/22/2021 7:29:46 PM,Saturday
日付や数がちゃんと入っていることが示されました。