きよくらの備忘録

「三日坊主と呼ばせない!日記」改め。主にソフトウェア開発関連の話題。

SQL Server Data Toolsのユニットテスト実行前に複数DBにデプロイする

SQL Server Data Tools (SSDT)のユニットテスト機能では、テスト実行前にソリューション内のデータベースプロジェクトをテスト実行対象のデータベースに自動でデプロイしてくれる機能があります。

これは非常に便利な機能です。しかし、複数のデータベースにまたがるような開発していると、依存関係があるなどの理由で複数のデータベースプロジェクトの内容をデプロイしたいことが出てきたりします。

SSDTの設定を調べて見ましたがそういった機能は用意されていなさそうで、一旦は諦めていました。が、別件で思案してたところでふと思いついたことを試してみた……ら、なんとか実現できなのでメモしておきます。

 

SSDTがユニットテスト実行前にデプロイしている仕組み

まず、SSDTがユニットテスト実行前にどうやってデプロイしているかを見てみます。

SSDTで作成されるテストプロジェクトでは、テストが実行されるとまずSqlDatabaseSetupクラスInitializeAssemblyメソッドが呼ばれます。 このメソッドはMsTest(Microsoft.VisualStudio.QualityTools.UnitTestFrameworkアセンブリ)のAssemblyInitialize属性がついた属性です。

この中で実行されているの処理は二つ。 そのうちのSqlDatabaseTestClass.TestService.DeployDatabaseProject();がデプロイ処理であることは名前からして疑う余地はなさそうです。

f:id:kiyokura:20161213232318p:plain

この処理が実装されているMicrosoft.Data.Tools.Schema.Sql.UnitTestingアセンブリのソースは公開されていませんので具体的にどのような処理をしているのかはわかりませんが、MSDNには一応載っていました。

SqlDatabaseTestService.DeployDatabaseProject Method

この DeployDatabaseProjectメソッドは、上記のMSDN

Deploys the database project by using the settings of the user in the app.config file.

と記載されている通り、app.confgのSqlUnitTestingセクションに記載された設定に従ってDBをデプロイします。この記載内容は、テストプロジェクト作成時や「SQL Server テスト構成」のダイアログで設定する内容です。

f:id:kiyokura:20161213232418p:plain

 

複数のDB・プロジェクトをデプロイする方法(強引)

追記:もっと適切だと思う方法がありましたので、ぜひそちらを参照してください kiyokura.hateblo.jp

SSDTが生成するユニットテストプロジェクトで自動デプロイが行われている仕組みをもう一度整理すると、こういうことです。

  • AssemblyInitialize属性の追加メソッド内でDeployDatabaseProjectメソッドでデプロイが実施される
  • DeployDatabaseProjectメソッド`はapp.configの設定に従って処理を行う

 

……ここまでくればピンと来たかもおられるでしょう。 そうです。

app.configを書き換えて DeployDatabaseProjectを呼ぶ

ようにすればいくらでもデプロイできそうじゃありませんか(

 

……ということで、例えば以下のようにapp.configを書き換えて実行してやることで、複数のデータベース/プロジェクトをすんなりデプロイすることができました。

using System;
using System.Configuration;
using Microsoft.Data.Tools.Schema.Sql.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Xml;

namespace BletillaTestsNext
{
  [TestClass()]
  public class SqlDatabaseSetup
  {
    // ターゲットデータベース
    const string TARGET_DB_PROJECT = "..\\..\\..\\ MainDb\\MainDb.sqlproj";
    const string TARGET_DB_CONNECTION_STRING = "<デプロイ先のDBの接続文字列>";
    
    // 依存データベース
    const string DEPEND_DB_PROJECT = "..\\..\\..\\SubDb\\SubDb.sqlproj";
    const string DEPEND_DB_CONNECTION_STRING = "<デプロイ先のDBの接続文字列> ";

    [AssemblyInitialize()]
    public static void InitializeAssembly(TestContext ctx)
    {
      // テスト実行ターゲットをデプロイ
      RewriteConfig(TARGET_DB_PROJECT, TARGET_DB_CONNECTION_STRING);
      SqlDatabaseTestClass.TestService.DeployDatabaseProject();
 
      // 依存DBをデプロイ
      RewriteConfig(DEPEND_DB_PROJECT, DEPEND_DB_CONNECTION_STRING);
      SqlDatabaseTestClass.TestService.DeployDatabaseProject();
 
      // テスト実行用に元に戻しておく
      RewriteConfig(TARGET_DB_PROJECT, TARGET_DB_CONNECTION_STRING);


      SqlDatabaseTestClass.TestService.GenerateData();
    }
    
    private static void RewriteConfig(string projectFile, string connectionString)
    {
      // デプロイは特権コンテキスト(PrivilegedContext)で行われるのでそちらだけ書き換えればよい
      var xmlDoc = new XmlDocument();
      xmlDoc.Load(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
      xmlDoc.SelectSingleNode("//SqlUnitTesting/DatabaseDeployment").Attributes["DatabaseProjectFileName"].Value = projectFile;
      xmlDoc.SelectSingleNode("//SqlUnitTesting/PrivilegedContext").Attributes["ConnectionString"].Value = connectionString;
      xmlDoc.Save(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
      ConfigurationManager.RefreshSection("SqlUnitTesting");
    }
  }
}

見てのとおり、app.configを読み込んで、

  • SqlUnitTesting/DatabaseDeploymentノードDatabaseProjectFileName属性に対象のプロジェクトファイルを
  • SqlUnitTesting/PrivilegedContextノードConnectionString属性にでデプロイ先のプロジェクトファイルを

書き込んで反映、念のためリフレッシュしています。

なお、SqlUnitTestingセクションで接続文字列を宣言してる箇所としてPrivilegedContextとExecutionContextの二つがありますが、この最初のでデプロイで利用されるのはPrivilegedContextですのでそこは間違えないようにする必要があります。 またプロジェクトファイルのパスは、このテストプロジェクトのアセンブリ出力パス(bin\Debug)からの相対パスで指定しています。

そのほかこの例ではプロジェクトのパスや接続文字列を直書きしてますが、実際にはapp.configにAppSettingsセクションを追加したりして管理するといいかもしれません。

 

まとめ

多少強引感もありますが、割とすんなり動いてる感があります。というか標準で複数デプロイ先へのデプロイに標準で対応してくれればもっと嬉しいのですが:p

DapperのQuery<dynamic>()の結果セットのフィールド名を取得する

ちょっと必要があったのでメモ。

 

Querydynamic()>が返すdynamicの実体はDapper.SqlMapper.DapperRowのコレクション

Query<dynamic()>()が返すdynamicの実体はDapper.SqlMapper.DapperRowのコレクションです。

f:id:kiyokura:20161209130932p:plain

 

このDapperRowはDapperのDapper.SqlMapperのprivateな型ですが、以下の通りIDictionary<string, object> の実装です。

github.com

 

ということで素直にIDictionary<string, object>にキャストしてみます。

  // SQLは実際にはSELECT * とか結果セット戻すストアドとか
  var result = cn.Query("SELECT 1 AS Id, 'Taro' AS Name , 20 AS Age"); 
  var fieldList = ((IDictionary<string, object>)result.First())
                    .Select(x => x.Key)
                    .ToList();

 

こんな感じで取れました。 f:id:kiyokura:20161209130950p:plain

 

どこでつかうん?

『どこでそんなもの使うの?』とか思う向きもかもしれないですが、クエリが返すフィールド名をハードコーディングしたくないケースって意外とあるというか、まあそんな感じで。 (例えばプログラム部分が『土管』に徹する場合(DB→JSONtとかDB→Excelとか)等だと、両端の柔軟性を生かす事ができると思います。というか、今回やりたいケースがまさにそれでした。)

ExcelとT4 と私とLinq to Excel

本エントリはC# Advent Calendar 2015 の11日目の記事です。

 

Excelからソースコードの自動生成というと何やらSIerめいた感を受ける方もおられるかもしれません。が、例えばExcel形式のデータベース定義などはコトの進め方によって普通にありだと思いますし、『柔軟なスキーマ定義ができ利用者の間口が広い高機能な編集ツールがあり場合によっては印刷や処理の自動化にも様々な方法で対応できるしかもオープンな規格*1のフォーマット』と捉えれば、やはり優秀なフォーマットだと言わざるを得ません(異論は認める)。 (個人的にはgitと親和性の高いdiffとマージを行う手段があれば割と満足というかそこが最大の難点*2

 

まあぶっちゃけそんなことは割とどうでもいい話で、何らかのコンテキストによってExcelからのソースコード自動生成が必要になった/それが最善だと判断したケースがあるとしましょう。きっとありますよ、そういうことも。

 

Excelの内容からコードを自動生成するにはいくつかアプローチがあると思います。 今回はT4からLinq to Excelを使う方法を紹介してみます。

 

Linq to Excel

その名のとおりExcelに対してLINQの文法でクエリを書けるものです。

オフィシャルなリソースはこちらあたりを参照

linqtoexcel - Use LINQ to retrieve data from spreadsheets. - Google Project Hosting

github.com

CSVから読めたり簡単にクラスとマップできたりと色々機能がありますが、そのあたりは割愛しますのでgithub上のreadmeをざざっと読んでもらえればと。

内部的にはJetまたはACEのOLEDBドライバを使ってアクセスしているようです。つまりはADO.NETのラッパー的な。

 

T4からLinq to Excel を使う

さて、ここからが本題です。T4からでもLinq to Excelを使うことも可能です。

T4とはVisual Studioで利用できるテキストテンプレートエンジンです。T4自体についての詳細はこの辺りを参照:コード生成と T4 テキスト テンプレート

 

Linq to Excelのインストール

Linq to ExcelそのものはNuGetでパッケージが提供されているのでパッケージインストーラーから簡単にインストールできます。

> Install-Package LinqToExcel

……できるんですが、現時点(LinqToExcel 1.10.1)では、log4netの参照周りでちょと問題があります。それについては後述。

 

参照の解決

T4からLinq to Excelを使いたい場合、LinqToExcel名前空間のインポートと、以下の依存するアセンブリの参照を行わないと怒られます。

  • System.Data
  • System.Data.DataSetExtensions
  • log4net.dll
  • Remotion.Data.Linq.dll
  • Remotion.Interfaces.dll
  • LinqToExcel.dll

特に下4つのdllの参照をどうするか……ですが、色々考えだすと面倒なので、適当にフォルダを作って必要なdllをすべてコピーしておき……

f:id:kiyokura:20151211222327p:plain

こんな感じで参照してやるほうがもう手っ取り早くて楽でよい気がしています。

f:id:kiyokura:20151211222341p:plain

……ズボラですかね:p

 

LinqToExcelを使ったコード

あとは実際にLinqToExcelを使ったT4を書けばよいだけです。 例えば、以下のようなテーブル定義ちっくなのExcelファイル「Def.xlsx」(シート名:People)があるとして。

項目名 サイズ 和名
Name NVARCHAR 50 氏名
Age INT 年齢
Email NVARCHAR 255 メールアドレス
Salary money 給料

以下のようなT4を書けば…

<#@ template debug ="false" hostspecific="true" language= "C#" #>
<#@ assembly name ="System.Core"  #>
<#@ import namespace ="System.Linq"  #>
<#@ import namespace ="System.Text"  #>
<#@ import namespace ="System.Collections.Generic"  #>
<#@ output extension =".cs" encoding="utf-8" #>

<#@ assembly name ="System.Data"  #>
<#@ assembly name ="System.Data.DataSetExtensions"  #>
<#@ assembly name ="$(ProjectDir)Lib\log4net.dll"  #>
<#@ assembly name ="$(ProjectDir)Lib\Remotion.Data.Linq.dll" #>
<#@ assembly name ="$(ProjectDir)Lib\Remotion.Interfaces.dll" #>
<#@ assembly name ="$(ProjectDir)Lib\LinqToExcel.dll"  #>
<#@ import namespace ="LinqToExcel"  #>
<#
  var xlsxPath = @"C:\Def.xlsx"; // フルパス
  var sheetName = "People";

  using(var excel = new ExcelQueryFactory(xlsxPath)){
    excel.ReadOnly = true;
    var columns =  excel.Worksheet<ColumnDef>(sheetName);
#>
public class People
{
<#
    foreach ( var column in columns){ #>
  /// <summary>
  /// <#= column.和名 #>
  /// </summary>
  public <#= GetType(column.型) #> <#= column.項目名 #> { get; set ; }

<#
    }
#>
}
<#
  }
#>
<#+
  // ここから下は本題じゃないので気にしないで下さい()
  static string GetType(string type){
    switch(type.ToUpper()){
      case "NVARCHAR":
        return "string" ;
      case "INT":
        return "int" ;
      case "MONEY":
        return "decimal" ;
      default:
        // 必要なマッピングは随時追加する雰囲気
        return "string" ;
    }
  }

  class ColumnDef {
    public string 項目名 { get ; set; }
    public string 型 { get ; set; }
    public int? サイズ { get ; set; }
    public string 和名 { get ; set; }
  }
#>

以下のようなC# のコードが自動生成されます。

public class People
{
  /// <summary>
  /// 氏名
  /// </summary>
  public string Name { get ; set; }

  /// <summary>
  /// 年齢
  /// </summary>
  public int Age { get ; set; }

  /// <summary>
  /// メールアドレス
  /// </summary>
  public string Email { get ; set; }

  /// <summary>
  /// 給料
  /// </summary>
  public decimal Salary { get ; set; }

}

……全然LINQのメリットを生かしてないサンプルコードですが、特にマッピングを定義することも無く、シート中の列名っぽいのとメンバ名を合わせたクラスを用意してファイルとシート名を指定して呼び出すだけで、さくっとマッピングして読み込んでくれるあたりは感じ取っていただけるのではないかと思います。

 

エラー対処というかはまりどころというか……について

T4…というよりLinqToExcelを使おうとすると、いくつかはまりどころがあるかもしれません。

 

log4netのバージョンが……

現在、LinqToExcel 1.10.1をNuGetでそのままインストールして利用すると、以下のようなエラーが出ました。

変換を実行しています:System.IO.FileNotFoundExceotion: ファイルまたはアセンブリ'log4net, Version=1.2.13.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a'、またはその依存関係の1つが読み込めませんでした。指定されたファイルが見つかりません。

ファイル名 'log4net, Version=1.2.13.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a' です。'log4net, Version=1.2.13.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a'

場所 LinqToExcel.ExcelQueryFactory..ctor(String fileName)

......

f:id:kiyokura:20151211222522p:plain

どうやら、LinqToExcelのパッケージがのlog4netの特定の(古い)バージョンに依存しているにもかかわらず、nuspecで最新のlog4netをインストールするように設定されているために起こっている齟齬です*3

とりあえず以下のようにやることで対処は可能です。

  • 一旦LinqToExcelのアンインストール
  • バージョンを指定してlog4netのインストール
  • LinqToExcelのインストール

lg4net 1.2.13はNuGetのパッケージでは2.0.3ですので、こんな感じで入れてしまいましょう。

Install-Package log4net -Version 2.0.3

ひとまずは現時点ではこれで動きました。

 

Microsoft.ACE.OLEDB.12.0' プロバイダーはローカルのコンピューターに登録されていません

別の例外でm

変換を実行しています:System.InvalidOperationException: 'Microsoft.ACE.OLEDB.12.0' プロバイダーはローカルのコンピューターに登録されていません。

場所 System.Data.OleDb.OleDbServicesWrapper.GetDataSource(OleDbConnectionString constr, DataSourceWrapper& datasrcWrapper)

場所 System.Data.OleDb.OleDbConnectionInternal..ctor(OleDbConnectionString constr, OleDbConnection connection)

......

となった場合は、内部的に使っているOLEDBプロバイダがインストールされていません(そのまんま)。これはMS-Officeに付属するものですが、単体でダウンロードしてインストールすることができます*4

Download 2007 Office system ドライバ: データ接続コンポーネント from Official Microsoft Download Center

なお、PCが64bitであっても、32bit版である必要があります*5

 

まとめ

ということで、T4からLinqToExcelを使ってExcel内の情報からコードを自動生成する方法を紹介してみました。

……が、改めてこれを書きながら冷静に見ると結構微妙という気になってきた気がしなくもない……ですが、一度導入してしまえばADO.NETとDapperあたり組み合わせるよりも手数が少なくて済むと思いますし、使う意味はあると思います。きっと。

log4netの変なバージョン依存だけはどうにかしてほしいですが、このプロジェクト年単位でメンテされてない感があるので(実際log4netの件はPRも来てるけど処置なし)、もう自分フォークしてビルドしたの使ったほうがいいんじゃないのという気がしてきたりもしたのでぼちぼちこのあたりで終わりたいと思います()

*1:2007以降のxlsx等の形式はOffice Open XMLというれっきとしたオープン企画です一応

*2:現状はOffice 2013 ProとかについてるSpred Sheet Compare とか使うとなんとかいけるかという感じ http://nasunoblog.blogspot.jp/2013/04/spreadsheet-compare.html

*3:プロジェクトファイルでのSpecificVersion=Falseの意味を取り違えているかnuspecの設定で意識していなかったのどちらかでしょうか

*4:これは@ahiru_spが遭遇したことで把握しました

*5:T4を実行するVisual Studioが32bitプロセスであるためだと思います

byte配列に読み込んだファイルの内容をMemoryStream上でzipファイルにする

.NET Framework 4.5 から、普通のZipファイルの圧縮・展開がとても簡単に扱えるようになりました。

例えばDBのblob(varbinary)列に格納された複数のファイル(のバイナリ)を読み出し、単一のZipファイルにアーカイブしてメモリストリーム上に保持するなら、こんな感じでできました。

// ※GetFilesFromDBはDBからファイルの情報を読みだして以下のようなクラスのIEnumerableなモノで返すと思ってくださいませ。
// class Hoge {
//   public string Name { get; set; }
//   public byte[] Bin { get; set; }
// }
var files = GetFilesFromDB();

using (var ms = new System.IO. MemoryStream())
{
  // メモリストリーム上にZipArchiveを作成する
  using ( var zipArchive = new ZipArchive(ms, ZipArchiveMode .Create, true ))
  {
    foreach ( var file in files)
    {
      // ファイルを追加していく。
      // ファイル名を指定してエントリを作って…
      var entry = zipArchive.CreateEntry(file.Name);
      using ( var es = entry.Open())
      {
        // エントリにバイナリを書き込む
        es.Write(file.Bin, 0, file.Bin.Length);
      }
    }
  }

 // あとはメモリストリーム (ms) を煮るなり焼くなり。
}

参考: ZipArchive クラス (System.IO.Compression)

AutoMapperを使ってオブジェクトを詰め替える

本日、ASP.NET MVC Do's and Dont's / Best Practices というまとめ記事が上がっていました*1

その中の一つ(少し古い記事ですが内容は大変に有用だと思います)の12 ASP.NET MVC Best Practicesを見ていたら、

DomainModelからViewModelに詰め替える時には退屈だしエラー積み込むかもしれんからバカ正直にコード書かずにAutoMapperとか使おうぜ(超訳)

とか書いてあるのを見つけました。

AutoMapper、そういえば以前どこかで割と普通なお方がお話されてた資料を見た記憶があるなぁ、と思いだしつつ*2、試してみることにしました。

AutoMapperとは?

AutoMapperとは、その名の通り(?)、オブジェクト同士を自動でマッピングしてくれるライブラリです。 以前はCodePlexで開発されていましたが、現在はGitHubがアクティブなリポジトリとなっているようです。

ざっくり概要を列挙するとこんなところでしょうか?

  • オブジェクトからオブジェクトにデータをコピーして詰め替える
  • コピーする際、プロパティ名の一致等の規約に沿って、自動でマップを行う
  • マッピングの規則はカスタマイズ可能
  • コレクションからコレクションへのマッピングも可能
  • NuGetで簡単に導入可能

さっそく、試してみましょう。 今回はシンプルに、コンソールアプリケーションのプロジェクトを使ってサンプルを作ってみます。

(以下のサンプルの完全なソリューション一色はgithubに置いてあります: kiyokura/AutoMapperSample · GitHub

 

 

AutoMapperの導入

AutoMapperの導入は、先に述べたとおりNuGetで一発で導入可能です。 パッケージ マネージャー コンソールから以下のようにインストールするか、GUIからAutoMapperで検索してインストールします。

PM> Install-Package AutoMapper

これだけで終了です。簡単ですね。

AutoMapperの基本

AutoMapperの基本的な使い方は次の2ステップのようです。

1. マップを作成

まず、Mapper.CreateMap<TSource, TDestination>()メソッドを使ってマップを作成する

2. マップを実行

Mapper.Map<TDestination>(object source)メソッドを使ってマップを実行

 

シンプルですね。 いくつか、実際のサンプルを動かしてみたいと思います。

 

 

サンプル1 : メンバーがまったく同じオブジェクトのマッピング

まずは一対一でオブジェクトをコピーするケースです

こんな二つのクラスがあるとして……

public class User
{
  public int ID { get; set; }
  public string Name { get; set; }
  public int Age { get; set; }
}

public class Member
{
  public int ID { get; set; }
  public string Name { get; set; }
  public int Age { get; set; }
}

基本に忠実に、こんな感じでサクッと。

// DBからとってきた(という体裁の)Memberクラスを、
// 全く同じメンバを持つ、User にマッピングする例

// Mapperを作成
Mapper.CreateMap<Member, User>();

// DBか何かからMember型でデータをとってきたと思ってください
Member member = new Member()
{
  ID = 15,
  Name = "Narami Kiyokura",
  Age = 20
};

//  Member型のmemberをUser型にマップ
User user = Mapper.Map<User>(member);


Console.WriteLine("ID:{0}", user.ID);
Console.WriteLine("Name:{0}", user.Name);
Console.WriteLine("Age:{0}", user.Age);

Console.ReadKey();

 

 

サンプル2 : コピー元とコピー先でメンバが異なる場合

とはいえ、コピー元と先でメンバが完全一致しないケースや、なんらかの加工が必要なケースもあると思います。以下程度の場合は、ごく簡単な加工追加処理だけで対応可能です。

こんな二つのクラスをマッピングしてみます。

public class Student
{
  public int ID { get; set; }
  public string Name { get; set; }
  public string Class { get; set; }
  public string Club { get; set; }
}


public class People
{
  public int PeopleID { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string Class { get; set; }
  public int PrefCode { get; set; }
}

お互いに存在しないメンバーについては、そのまま何もしなくてもエラーにならず、無視されます。 その他の場合は、マップ時にForMember()メソッドを使ってマップを定義してあげることができます。

// DBからとってきた(という体裁の)Peopleクラスを、
// 一部異なる名前のフィールドを持つStudent にマッピングする例

// Mapperを作成
// .ForMemberメソッドで移行先の何をセットするか指定する
// Student.ID に PeopleID をマップ
// Student.Name に People.FirstName とPeople.LastName を組み立てた値をマップ
// Student.Class にはClassはマップしない
// 
// お互いに存在しないものは何もしなくても無視する(People.PrefCodeやStudent.Club)
Mapper.CreateMap<People, Student>()
  .ForMember(d => d.ID, o => o.MapFrom(s => s.PeopleID))
  .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName + " " + s.LastName))
  .ForMember(d => d.Class, o => o.Ignore());

// DBか何かからPeople型でデータをとってきたと思ってください
People people = new People()
{
  PeopleID = 15,
  FirstName = "Narami",
  LastName = "Kiyokura",
  Class = "これはマップしない",
  PrefCode = 33
};

//  People型のpeopleをStudent型にマップ
Student student = Mapper.Map<Student>(people);


Console.WriteLine("ID:{0}", student.ID);
Console.WriteLine("Name:{0}", student.Name);
Console.WriteLine("Class:{0}", student.Class);
Console.WriteLine("Club:{0}", student.Club);

Console.ReadKey();

もっと複雑になる場合も対応可能ですが、今回は割愛します。 (公式ドキュメントに詳しくいろいろあります)

 

 

サンプル3 : コレクションからコレクションへのマッピング

最後に、コレクションからコレクションへのマップを試してみたいと思います。これも利用する頻度は高くなりそうです。

サンプル1で利用したMemberUserを利用します。

// DBからとってきた(という体裁の)Memberクラスのコレクションを、
// 全く同じメンバを持つ、Userのコレクション にマッピングする例

// Mapperを作成
// ここで指定するのはMember型とUser型
Mapper.CreateMap<Member, User>();

// DBか何かからMember型のコレクションでデータをとってきたと思ってください
List<Member> members = new List<Member>(){
    new Member()
    {
      ID = 15,
      Name = "Narami Kiyokura",
      Age = 20
    },
                        new Member()
    {
      ID = 17,
      Name = "Taro Yamada",
      Age = 25
    }
  };

//  Member型のmemberをUser型にマップ
List<User> users = Mapper.Map<List<User>>(members);

foreach (var user in users)
{
  Console.WriteLine("ID:{0}", user.ID);
  Console.WriteLine("Name:{0}", user.Name);
  Console.WriteLine("Age:{0}", user.Age);
}


Console.ReadKey();

ポイントは以下でしょうか - Mapの作成時はクラス(コレクションではない)を指定 - Mapper.Map<T>()の型引数にコレクション型を指定

これも結構シンプルにできますね。

 

 

まとめ

ということで、AutoMapperをざっくり触ってみました。超便利ですね。

他にも、Flatingといって、ネストされたオブジェクトに対して命名即に沿った形で自動的にフラットなオブジェクトにマッピングする機能も持っていたりします。

AutoMapperについて少し検索してみると、5年近く前にMVP for ASP.NET/IIS の青木さんが紹介記事『AutoMapperでオブジェクト間のデータコピーを行う』を執筆されておられたことを知りました。

青木さんの記事では、私が上記で試していないパターンやASP.NET MVCで実用的に使う場合のマッピング定義の扱い方などについても書かれておられます。AutoMapperに興味を持たれた方は是非、目を通されることをお勧めしたいと思います。

 

 

参考

*1:ちなみにこのサイトはマイクロソフトがホストしているキュレーションサイト『Curah!』(読み:キュラー)という、いわゆるまとめサイト的な奴 : http://curah.microsoft.com/

*2:割と普通氏のスライド資料はこちら:http://www.slideshare.net/normalian/clrh-69