web-dev-qa-db-ja.com

LINQ to Entitiesは、IEntityインターフェイスを使用したEDMプリミティブまたは列挙型のキャストのみをサポートします

次の汎用拡張メソッドがあります。

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を機能させるにはどうすればよいですか?

89
Steven

classジェネリック型制約を拡張メソッドに追加することで、これを解決できました。ただし、なぜ機能するのかはわかりません。

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}
177
Sam

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#ウィザードでこれを説明できることを願っています。

61
Tadej Mali

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);
    }

    ...
}
21
Steven

私は同じエラーを抱えていましたが、似ているが異なる問題がありました。 IQueryableを返す拡張関数を作成しようとしていましたが、フィルター条件は基本クラスに基づいていました。

私は最終的に、拡張メソッドが.Select(e => e as T)を呼び出すためのソリューションを見つけました。ここで、Tは子クラスで、eは基本クラスです。

詳細はこちら: EFの基本クラスを使用してIQueryable <T>拡張機能を作成

1
Justin