LINQ to Entities obsługuje tylko rzutowanie typów podstawowych lub wyliczeniowych EDM z interfejsem IEntity

96

Mam następującą ogólną metodę rozszerzenia:

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

Niestety Entity Framework nie wie, jak obsłużyć, predicateponieważ C # przekonwertował predykat na następujący:

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

Entity Framework zgłasza następujący wyjątek:

Nie można rzutować typu „IEntity” na typ „SomeEntity”. LINQ to Entities obsługuje tylko rzutowanie typów podstawowych lub wyliczeniowych EDM.

Jak możemy sprawić, by Entity Framework działał z naszym IEntityinterfejsem?

Steven
źródło

Odpowiedzi:

188

Udało mi się rozwiązać ten problem, dodając classogólne ograniczenie typu do metody rozszerzenia. Nie jestem jednak pewien, dlaczego to działa.

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}
Sam
źródło
6
U mnie też działa! Bardzo bym chciał, żeby ktoś mógł to wyjaśnić. #linqblackmagic
berko,
Czy możesz wyjaśnić, w jaki sposób dodałeś to ograniczenie
yrahman
5
Domyślam się, że używany jest typ klasy, a nie typ interfejsu. EF nie wie o typie interfejsu, więc nie może go przekonwertować na SQL. W przypadku ograniczenia klasy wywnioskowanym typem jest typ DbSet <T>, z którym EF wie, co zrobić.
jwize
1
Idealnie, wspaniale jest móc wykonywać zapytania oparte na interfejsie i nadal utrzymywać kolekcję jako IQueryable. Trochę denerwujące jest jednak to, że w zasadzie nie ma sposobu na wymyślenie tej poprawki bez znajomości wewnętrznego działania EF.
Anders
To, co tu widzisz, to ograniczenie czasowe kompilatora, które pozwala kompilatorowi C # określić, że T jest typu IEntity w metodzie, dzięki czemu jest w stanie określić, że jakiekolwiek użycie elementu IEntity jest poprawne, tak jak podczas kompilacji wygenerowany kod MSIL automatycznie przeprowadzi tę kontrolę przed rozmową. Aby wyjaśnić, dodanie „class” jako ograniczenia typu w tym miejscu umożliwia poprawne działanie funkcji collection.FirstOrDefault (), ponieważ prawdopodobnie zwraca nową instancję T wywołującą domyślny ctor na typie opartym na klasie.
Wojna
64

Dodatkowe wyjaśnienia dotyczące class„poprawki”.

Ta odpowiedź pokazuje dwa różne wyrażenia, jedno z, a drugie bez where T: classograniczeń. Bez classograniczeń mamy:

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

iz ograniczeniem:

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

Te dwa wyrażenia są różnie traktowane przez strukturę encji. Patrząc na źródła EF 6 , można stwierdzić, że wyjątek pochodzi stąd, patrzValidateAndAdjustCastTypes() .

Dzieje się tak, że EF próbuje rzutować IEntityna coś, co ma sens w świecie modelu domeny, jednak nie udaje się to, dlatego zgłaszany jest wyjątek.

Wyrażenie z class ograniczeniem nie zawiera Convert()operatora, rzutowanie nie jest wypróbowywane i wszystko jest w porządku.

Wciąż pozostaje otwarte pytanie, dlaczego LINQ buduje różne wyrażenia? Mam nadzieję, że jakiś kreator C # będzie w stanie to wyjaśnić.

Tadej Mali
źródło
1
Dziękuję za wyjaśnienie.
Jace Rhea
9
@JonSkeet ktoś próbował przywołać tutaj kreatora C #. Gdzie jesteś?
Nick N.
23

Entity Framework nie obsługuje tego po wyjęciu z pudełka, ale ExpressionVisitormożna łatwo napisać wyrażenie, które tłumaczy:

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

Jedyne, co musisz zrobić, to przekonwertować przekazany predykat za pomocą gościa wyrażenia w następujący sposób:

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

    ...
}

Innym - mało elastycznym - podejściem jest wykorzystanie 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
źródło
1

Miałem ten sam błąd, ale podobny, ale inny problem. Próbowałem utworzyć funkcję rozszerzającą, która zwróciła IQueryable, ale kryteria filtru były oparte na klasie bazowej.

W końcu znalazłem rozwiązanie, które było dla mojej metody rozszerzającej do wywołania .Select (e => e jako T), gdzie T jest klasą potomną, a e jest klasą bazową.

pełne szczegóły są tutaj: Utwórz rozszerzenie IQueryable <T> przy użyciu klasy bazowej w EF

Justin
źródło