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

1
Ідеально, це чудово, коли можна виконувати запити на основі інтерфейсу і при цьому підтримувати колекцію як IQueryable. Однак трохи дратує те, що в основному неможливо придумати це виправлення, не знаючи внутрішньої роботи EF.
Андерс,

Те, що ви бачите тут, - це обмеження часу компілятора, яке дозволяє компілятору C # визначити, що T має тип IEntity у методі, тому може визначити, що будь-яке використання "речей" IEntity є дійсним, як під час компіляції, згенерований код MSIL автоматично здійснить для вас цю перевірку до дзвінка. Для уточнення, додавання тут «класу» як обмеження типу дозволяє колекції.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
Дякую за пояснення.
Jace Rhea

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, але критерії фільтрації базувались на базовому класі.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.