LINQ to Entities поддерживает только приведение примитивов EDM или перечислимых типов с интерфейсом IEntity.

96

У меня есть следующий общий метод расширения:

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

К сожалению, Entity Framework не знает, как справиться с этим, predicateпоскольку C # преобразовал предикат в следующее:

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

Entity Framework выдает следующее исключение:

Невозможно привести тип IEntity к типу SomeEntity. LINQ to Entities поддерживает только приведение примитивов EDM или перечислимых типов.

Как мы можем заставить Entity Framework работать с нашим IEntityинтерфейсом?

Стивен
источник

Ответы:

188

Я смог решить эту проблему, добавив classограничение универсального типа в метод расширения. Однако я не уверен, почему это работает.

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}
Сэм
источник
6
У меня тоже работает! Я хотел бы, чтобы кто-нибудь смог это объяснить. #linqblackmagic
berko
Не могли бы вы объяснить, как вы добавили это ограничение?
yrahman
5
Я предполагаю, что используется тип класса, а не тип интерфейса. EF не знает тип интерфейса, поэтому не может преобразовать его в SQL. С ограничением класса выводимым типом является тип DbSet <T>, с которым EF знает, что делать.
jwize
2
Отлично, здорово иметь возможность выполнять запросы на основе интерфейса и при этом поддерживать коллекцию как IQueryable. Однако немного раздражает то, что в принципе невозможно придумать это исправление, не зная внутренней работы EF.
Андерс
Здесь вы видите временное ограничение компилятора, которое позволяет компилятору C # определять, что T имеет тип IEntity внутри метода, поэтому он может определить, что любое использование IEntity "материала" допустимо, поскольку во время компиляции сгенерирован код MSIL автоматически выполнит эту проверку перед звонком. Чтобы уточнить, добавление «класса» в качестве ограничения типа здесь позволяет collection.FirstOrDefault () работать правильно, поскольку он, вероятно, возвращает новый экземпляр T, вызывающий ctor по умолчанию для типа на основе класса.
Война
64

Некоторые дополнительные пояснения по поводу class"исправления".

Этот ответ показывает два разных выражения, одно с where T: classограничением, а другое без ограничения. Без classограничения мы имеем:

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

и с ограничением:

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

Эти два выражения обрабатываются структурой сущности по-разному. Посмотрев на источники EF 6 , можно обнаружить, что исключение исходит отсюда, смValidateAndAdjustCastTypes() .

Происходит то, что EF пытается IEntityпреобразовать во что-то, что имеет смысл в мире модели предметной области, однако это не удается, поэтому создается исключение.

Выражение с classограничением не содержит Convert()оператора, приведение не выполняется и все в порядке.

Остается открытым вопрос, почему LINQ строит разные выражения? Я надеюсь, что какой-нибудь мастер C # сможет это объяснить.

Тадей Мали
источник
1
Спасибо за объяснение.
Джейс Реа
9
@JonSkeet здесь кто-то пытался вызвать мастера C #. Где ты?
Ник Н.
23

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

    ...
}

Другой - менее гибкий - подход заключается в использовании 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);
    }

    ...
}
Стивен
источник
1

У меня была такая же ошибка, но похожая, но другая проблема. Я пытался создать функцию расширения, которая возвращала бы IQueryable, но критерии фильтрации были основаны на базовом классе.

В конце концов я нашел решение, которое было для моего метода расширения для вызова .Select (e => e as T), где T - дочерний класс, а e - базовый класс.

полная информация здесь: Создание расширения IQueryable <T> с использованием базового класса в EF

Джастин
источник