IEnumerable<T> を DataTable に変換する
このご時世に DataTable かよ、という感じもしますし、私自身実案件では Dapper をメインで使用しているわけですが、少しは使える場面もあるかもしれません。
例えばデバッガで使える DataSet ビジュアライザ。こういうやつですね。
これなら複数レコードのプロパティ/フィールドの値を一覧で見ることができます。単に CSV やタブ区切りテキストに吐き出すという手もありますが、デバッグ中にシームレスに見られるというのはやはり便利です。少々重いですが。
他はまぁ、WinForms や WebForms を使っている場合ですかね。特に前者の場合、DataGridView のソートやフィルタを使うには IBindingList や IBindingListView インターフェースが必要なわけですが、これらを実装しているのはベースクラスライブラリでは DataView だけという状況で、自前で実装するのも相当骨が折れるので。
というわけで、いわゆる業務ロジックや DB アクセスには POCO を使い、UI でグリッドを使う時だけ DataTable に変換するという使い方ができるかもしれません(?)
というわけで作ってみましょう。まずはプロパティやフィールドの情報を基に DataColumn を作成するメソッドを用意します。
private static DataColumn ToDataColumn(this PropertyInfo propInfo) { return ToDataColumnCore(propInfo.PropertyType, propInfo.Name); } private static DataColumn ToDataColumn(this FieldInfo fieldInfo) { return ToDataColumnCore(fieldInfo.FieldType, fieldInfo.Name); } private static DataColumn ToDataColumnCore(Type type, String name) { var isNullable = (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); var columnType = isNullable ? Nullable.GetUnderlyingType(type) : type; var column = new DataColumn(name, columnType); column.AllowDBNull = (isNullable || !type.IsValueType); return column; }
私はわりとカジュアルに拡張メソッド作っちゃう派です。どうせ private だし。
型が Nullable ないし参照型の場合は DBNull を許すようにしてます。
あとは IEnumerable
引数に BindingFlags を指定することで、プロパティかフィールドのどちらか片方だけ出力することもできるようなオーバーロードも付けてます。
public static DataTable ToDataTable<T>(this IEnumerable<T> entities) { return entities.ToDataTable(BindingFlags.GetField | BindingFlags.GetProperty); } public static DataTable ToDataTable<T>(this IEnumerable<T> entities, BindingFlags bindingAttr) { if ((bindingAttr & ~BindingFlags.GetField & ~BindingFlags.GetProperty) != BindingFlags.Default) { throw new ArgumentException(nameof(bindingAttr)); } var table = new DataTable(typeof(T).Name); var getters = new Dictionary<DataColumn, Func<T, object>>(); var hasItem = entities.Any(); Action<Func<DataColumn>, Func<ParameterExpression, MemberExpression>> buildConverter = (getColumn, getMember) => { var column = getColumn(); table.Columns.Add(column); if (hasItem) { var param = Expression.Parameter(typeof(T), "entity"); var member = Expression.Convert(getMember(param), typeof(object)); var lambda = Expression.Lambda<Func<T, object>>(member, param); getters.Add(column, lambda.Compile()); } }; if ((bindingAttr & BindingFlags.GetProperty) == BindingFlags.GetProperty) { foreach (var property in typeof(T).GetProperties()) { buildConverter(() => property.ToDataColumn(), (param) => Expression.Property(param, property)); } } if ((bindingAttr & BindingFlags.GetField) == BindingFlags.GetField) { foreach (var field in typeof(T).GetFields()) { buildConverter(() => field.ToDataColumn(), (param) => Expression.Field(param, field)); } } foreach (var entity in entities) { var row = table.NewRow(); row.BeginEdit(); foreach (var getter in getters) { row[getter.Key] = getter.Value(entity) ?? DBNull.Value; } row.EndEdit(); table.Rows.Add(row); } table.AcceptChanges(); return table; }
本当はもう少しオーバーロード追加したり、テストメソッドも用意した上で GitHub に上げたかったんですが、ただの Git すらよくわかってないマンなので、今日のところはこれまで。
ExpressionTree の PropertyOrField
ExpressionTree でプロパティやフィールドの値を取得するとき、Expression.Property や Expression.Field を使えば良いんですが、Expression.PropertyOrField なんてのもあり、こっちだとその名の通りプロパティでもフィールドでもどっちでも OK みたいです。
じゃあプロパティかフィールドかは気にせず常に PropertyOrField 使えば楽チンじゃん、っと思いたいところですが、なんかのオーバーヘッドがあって Property や Field 使うよりちょっとだけ遅いんじゃ?という気もします。というわけで調べてみましょ。
こんなコードを書いて、それぞれの式がどんな風にコンパイルされるかを見てみました。
class Program { static void Main(string[] args) { var foo = Expression.Constant(new Foo { Property1 = 1, Field1 = 2}); // Property でプロパティにアクセス var prop1 = Expression.Property(foo, "Property1"); // PropertyOrField でプロパティにアクセス var prop2 = Expression.PropertyOrField(foo, "Property1"); // Field でフィールドにアクセス var fld1 = Expression.Field(foo, "Field1"); // PropertyOrField でフィールドにアクセス var fld2 = Expression.PropertyOrField(foo, "Field1"); Console.ReadKey(); } } class Foo { public int Property1 { get; set; } public int Field1; }
式ツリーのデバッグによると、DebugView プロパティで ExressionTree の中身が見られるらしいですね。というわけで早速見てみましょう。
あの、見えないんですけど...。なんでなんでなんで???
どうやら DebugView を使用するためには、.Net Framework のバージョンを 4 以上にしないといけないらしいです。SIer に勤めてると .Net 3.5 縛りの案件がまだまだ多くて、そのノリを引きずってしまっていました。
というわけで、気を取り直して現在の最新である .Net 4.6.1 に設定して再チャレンジ。
今度は OK ですね。この調子で他の式も見てみましょう。結果は次の通り。
- prop1(Property)
- .Constant
(PropertyOrField.Foo).Property1 - prop2(PropertyOrField)
- .Constant
(PropertyOrField.Foo).Property1 - fld1(Field)
- .Constant
(PropertyOrField.Foo).Field1 - fld2(PropertyOrField)
- .Constant
(PropertyOrField.Foo).Field1
Property/Field を使い分けた場合も PropertyOrField の場合も作られる式に特に違いはないみたいですね。
もしかすると式自体を構築するときにごくわずかな性能差があるかもしれないですが、コンパイルした式が場合によっては何百万回とか呼ばれるのに対し、式の構築は最初の 1 回だけなので気にするレベルじゃないですね。
というわけで、積極的に PropertyOrField を使って OK ですね。
2016/2/21 追記
パフォーマンスとは別の問題で PropertyOrField が適さないケースがあったので追記します。
大文字の使用規則ガイドラインに反するので普通はやらないし、VB だとそもそもできなかったりしますが、型のメンバに例えば Foo と foo のように大文字と小文字だけが異なるメンバが混在している場合、PropertyOrField でアクセスしようとすると AmbiguousMatchException が発生してしまいます。大文字小文字を区別させるオーバーロードは残念ながら存在しません。
そのような場合は Property(Expression, PropertyInfo) などを使う必要があります。