LinqToSQL、Concat() で InvalidCastException

標準値のリストと、特別な条件に合致する場合にのみ適用される専用値のリストがある。

CREATE TABLE standard_salaries (job DECIMAL NOT NULL, salary DECIMAL NOT NULL)
primary key (job)

CREATE TABLE special_salaries (rank DECIMAL NOT NULL, job DECIMAL NOT NULL, salary DECIMAL NOT NULL)
primary key (rank, job)

特別な条件(この例では rank) が NULL を許容しないため、標準値を示す rank を NULL として扱って rank と job をキー、salary を値にした辞書を作成しようと、

var defaults = from std in context.standard_salaries
                select new { rank = new int?(), std.job, std.salary }

var specials = from sp in context.special_salaries
                select new { rank = new int?(sp.rank), sp.job, sp.salary }

というようなクエリを Concat() で UNION ALL して ToDictionary() を呼び出そうとしたら例外が飛んだ。

// InvalidCastException が飛ぶ
foreach (var s in defaults.Concat(specials))
    Console.WriteLine("{0}: {1} ({2})", s.job, s.salary, s.rank);

// InvalidCastException が飛ばない
foreach (var s in specials.Concat(defaults))
    Console.WriteLine("{0}: {1} ({2})", s.job, s.salary, s.rank);

例外のコールスタックは defaults の列挙がすべて終わった後、specials の1件目のレコードを取り出すための ObjectDataReader.MoveNext() の内部を示している。
内部の動作をまったく調べずに想像だけで書くと、LinqToSQL の属性ベースマッピングの場合、defaults.Expression (IQueryable.Expression) からメタデータを取得できないのが原因ではないだろうか?と思う。
Concat() は匿名型の互換性だけを調べて2つのクエリを接続可能と判断し、LinqToSQL は個々のクエリの IQueryable.Expression から SQL 文を作成して Concat() による UNION ALL 句を生成する。このクエリがインスタンス化される時、ObjectDataReader は SQL 文の実行結果セットから出力を生成・初期化する LightWeight コードを作成するわけだが、そこでは結果セットの個々のフィールドに保持したデータを SQL データ型から出力型のプロパティ型へのデータ変換が実装される。この変換の実装には変換元のデータ型と変換先データ型が必要なのだが、defaults.Expression に含まれる匿名型の rank プロパティや、その作成処理をどうひっくり返しても変換元データ型を保持しているメタデータへは到達できない。Concat() の順序を入れ替えると正常に動作するのは、specials の IQueryable.Expression がメタデータへ到達できるだけの十分な情報を保持しているからではないだろうか?