次の汎用拡張メソッドがあります。
public static T GetById<T>(this IQueryable<T> collection, Guid id)
where T : IEntity
{
Expression<Func<T, bool>> predicate = e => e.Id == id;
T entity;
// Allow reporting more descriptive error messages.
try
{
entity = collection.SingleOrDefault(predicate);
}
catch (Exception ex)
{
throw new InvalidOperationException(string.Format(
"There was an error retrieving an {0} with id {1}. {2}",
typeof(T).Name, id, ex.Message), ex);
}
if (entity == null)
{
throw new KeyNotFoundException(string.Format(
"{0} with id {1} was not found.",
typeof(T).Name, id));
}
return entity;
}
残念ながら、C#は述語を次のように変換したため、Entity Frameworkはpredicate
を処理する方法を知りません。
e => ((IEntity)e).Id == id
Entity Frameworkは次の例外をスローします。
タイプ「IEntity」をタイプ「SomeEntity」にキャストできません。 LINQ to Entitiesは、EDMプリミティブまたは列挙型のキャストのみをサポートしています。
IEntity
インターフェイスでEntity Frameworkを機能させるにはどうすればよいですか?
class
ジェネリック型制約を拡張メソッドに追加することで、これを解決できました。ただし、なぜ機能するのかはわかりません。
public static T GetById<T>(this IQueryable<T> collection, Guid id)
where T : class, IEntity
{
//...
}
class
「修正」に関する追加説明。
この回答 は、2つの異なる式を示しています。1つは_where T: class
_制約のある式とない式です。 class
制約がない場合、次のようになります。
_e => e.Id == id // becomes: Convert(e).Id == id
_
そして制約付き:
_e => e.Id == id // becomes: e.Id == id
_
これら2つの式は、エンティティフレームワークによって異なる方法で処理されます。 EF 6のソース を見ると、例外は ここではValidateAndAdjustCastTypes()
からのものであることがわかります。
EFはIEntity
をドメインモデルの世界に意味のあるものにキャストしようとしますが、そうすることに失敗したため、例外がスローされます。
class
制約のある式にはConvert()
演算子が含まれていません。キャストは試行されず、すべて正常です。
LINQが異なる表現を作成する理由は未解決のままです。一部のC#ウィザードでこれを説明できることを願っています。
Entity Frameworkはすぐにこれをサポートしていませんが、式を翻訳するExpressionVisitor
は簡単に記述できます。
private sealed class EntityCastRemoverVisitor : ExpressionVisitor
{
public static Expression<Func<T, bool>> Convert<T>(
Expression<Func<T, bool>> predicate)
{
var visitor = new EntityCastRemoverVisitor();
var visitedExpression = visitor.Visit(predicate);
return (Expression<Func<T, bool>>)visitedExpression;
}
protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType == ExpressionType.Convert && node.Type == typeof(IEntity))
{
return node.Operand;
}
return base.VisitUnary(node);
}
}
必要なのは、次のように式ビジターを使用して、渡された述語を変換することだけです。
public static T GetById<T>(this IQueryable<T> collection,
Expression<Func<T, bool>> predicate, Guid id)
where T : IEntity
{
T entity;
// Add this line!
predicate = EntityCastRemoverVisitor.Convert(predicate);
try
{
entity = collection.SingleOrDefault(predicate);
}
...
}
もう1つの柔軟性の低いアプローチは、DbSet<T>.Find
:
// NOTE: This is an extension method on DbSet<T> instead of IQueryable<T>
public static T GetById<T>(this DbSet<T> collection, Guid id)
where T : class, IEntity
{
T entity;
// Allow reporting more descriptive error messages.
try
{
entity = collection.Find(id);
}
...
}
私は同じエラーを抱えていましたが、似ているが異なる問題がありました。 IQueryableを返す拡張関数を作成しようとしていましたが、フィルター条件は基本クラスに基づいていました。
私は最終的に、拡張メソッドが.Select(e => e as T)を呼び出すためのソリューションを見つけました。ここで、Tは子クラスで、eは基本クラスです。