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