本エントリは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
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をすべてコピーしておき……
こんな感じで参照してやるほうがもう手っ取り早くて楽でよい気がしています。
……ズボラですかね:p
LinqToExcelを使ったコード
あとは実際にLinqToExcelを使ったT4を書けばよいだけです。 例えば、以下のようなテーブル定義ちっくなのExcelファイル「Def.xlsx」(シート名:People)があるとして。
項目名 | 型 | サイズ | 和名 |
---|---|---|---|
Name | NVARCHAR | 50 | 氏名 |
Age | INT | 年齢 | |
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)
......
どうやら、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の設定で意識していなかったのどちらかでしょうか
*5:T4を実行するVisual Studioが32bitプロセスであるためだと思います