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

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

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, поскольку С# преобразовал предикат в следующее:

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

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

Невозможно ввести тип "IEntity" для ввода "SomeEntity". LINQ to Объекты поддерживают только листинг EDM-примитивов или типов перечисления.

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

Ответ 1

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

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

Ответ 2

Некоторые дополнительные пояснения относительно исправления 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 строит разные выражения? Я надеюсь, что некоторые мастера С# смогут это объяснить.

Ответ 3

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

    ...
}

Ответ 4

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

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

полная информация здесь: создайте расширение IQueryable <T>, используя базовый класс в EF