LINQ to Entities unterstützt nur das Umwandeln von EDM-Grund- oder Aufzählungstypen mit IEntity-Schnittstelle

96

Ich habe die folgende generische Erweiterungsmethode:

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

Leider weiß Entity Framework nicht, wie es damit umgehen soll, predicateda C # das Prädikat in Folgendes konvertiert hat:

e => ((IEntity)e).Id == id

Entity Framework löst die folgende Ausnahme aus:

Der Typ 'IEntity' kann nicht in 'SomeEntity' umgewandelt werden. LINQ to Entities unterstützt nur das Umwandeln von EDM-Grund- oder Aufzählungstypen.

Wie können wir Entity Framework mit unserer IEntitySchnittstelle zum Laufen bringen?

Steven
quelle

Antworten:

188

Ich konnte dies beheben, indem ich classder Erweiterungsmethode die generische Typbeschränkung hinzufügte . Ich bin mir jedoch nicht sicher, warum es funktioniert.

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}
Sam
quelle
6
Funktioniert auch bei mir! Ich würde es lieben, wenn jemand dies erklären könnte. #linqblackmagic
berko
Können Sie bitte erklären, wie Sie diese Einschränkung
hinzugefügt haben
5
Ich vermute, dass der Klassentyp anstelle des Schnittstellentyps verwendet wird. EF kennt den Schnittstellentyp nicht und kann ihn daher nicht in SQL konvertieren. Mit der Klassenbeschränkung ist der abgeleitete Typ der DbSet <T> -Typ, mit dem EF zu tun weiß.
Jwize
1
Perfekt, es ist großartig, Schnittstellen-basierte Abfragen durchführen zu können und die Sammlung dennoch als IQueryable zu verwalten. Ein bisschen ärgerlich ist jedoch, dass es im Grunde keine Möglichkeit gibt, sich dieses Update auszudenken, ohne das Innenleben von EF zu kennen.
Anders
Was Sie hier sehen, ist eine Compiler-Zeitbeschränkung, die es dem C # -Compiler ermöglicht, zu bestimmen, dass T vom Typ IEntity innerhalb der Methode ist, sodass festgestellt werden kann, dass jede Verwendung von IEntity "stuff" gültig ist, wie während der Kompilierungszeit, die der MSIL-Code generiert hat führt diese Prüfung vor dem Anruf automatisch für Sie durch. Um dies zu verdeutlichen, kann durch Hinzufügen von "Klasse" als Typbeschränkung collection.FirstOrDefault () korrekt ausgeführt werden, da wahrscheinlich eine neue Instanz von T zurückgegeben wird, die einen Standard-ctor für einen klassenbasierten Typ aufruft.
Krieg
64

Einige zusätzliche Erklärungen zum class"Fix".

Diese Antwort zeigt zwei verschiedene Ausdrücke, einen mit und einen ohne where T: classEinschränkung. Ohne die classEinschränkung haben wir:

e => e.Id == id // becomes: Convert(e).Id == id

und mit der Einschränkung:

e => e.Id == id // becomes: e.Id == id

Diese beiden Ausdrücke werden vom Entity-Framework unterschiedlich behandelt. Wenn man sich die EF 6-Quellen ansieht , kann man feststellen, dass die Ausnahme von hier kommt, sieheValidateAndAdjustCastTypes() .

Was passiert ist, dass EF versucht, IEntityin etwas zu verwandeln, das für die Domain-Modellwelt sinnvoll ist, dies jedoch fehlschlägt, weshalb die Ausnahme ausgelöst wird.

Der Ausdruck mit der classEinschränkung enthält nicht den Convert()Operator, die Umwandlung wird nicht versucht und alles ist in Ordnung.

Es bleibt die Frage offen, warum LINQ unterschiedliche Ausdrücke erstellt. Ich hoffe, dass ein C # -Werker dies erklären kann.

Tadej Mali
quelle
1
Danke für die Erklärung.
Jace Rhea
9
@ JonSkeet Jemand hat versucht, hier einen C # -Zauberer zu beschwören. Wo bist du?
Nick N.
23

Entity Framework unterstützt dies nicht sofort, aber ein ExpressionVisitorAusdruck, der den Ausdruck übersetzt, ist einfach zu schreiben:

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

Das einzige, was Sie tun müssen, ist, das übergebene Prädikat mit dem Ausdruck Besucher wie folgt zu konvertieren:

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

    ...
}

Ein anderer flexibler Ansatz besteht darin, Folgendes zu nutzen 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);
    }

    ...
}
Steven
quelle
1

Ich hatte den gleichen Fehler, aber ein ähnliches, aber unterschiedliches Problem. Ich habe versucht, eine Erweiterungsfunktion zu erstellen, die IQueryable zurückgibt, aber die Filterkriterien basieren auf der Basisklasse.

Ich fand schließlich die Lösung für meine Erweiterungsmethode .Select (e => e als T), wobei T die untergeordnete Klasse und e die Basisklasse ist.

Ausführliche Informationen finden Sie hier: Erstellen Sie die IQueryable <T> -Erweiterung mithilfe der Basisklasse in EF

Justin
quelle