LINQ ke Entitas hanya mendukung casting EDM primitif atau jenis enumerasi dengan antarmuka IEntity

96

Saya memiliki metode ekstensi umum berikut:

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

Sayangnya Entity Framework tidak tahu bagaimana menangani predicatesejak C # mengonversi predikat menjadi berikut ini:

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

Entity Framework menampilkan pengecualian berikut:

Tidak dapat mentransmisikan jenis 'IEntity' untuk mengetik 'SomeEntity'. LINQ ke Entitas hanya mendukung casting EDM primitif atau jenis enumerasi.

Bagaimana kami dapat membuat Entity Framework berfungsi dengan IEntityantarmuka kami ?

Steven
sumber

Jawaban:

188

Saya dapat menyelesaikan ini dengan menambahkan classbatasan tipe umum ke metode ekstensi. Saya tidak yakin mengapa itu berhasil.

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}
Sam
sumber
6
Bekerja untuk saya juga! Saya ingin seseorang dapat menjelaskan ini. #linqblackmagic
berko
Bisakah Anda menjelaskan bagaimana Anda menambahkan batasan ini
yrahman
5
Dugaan saya adalah bahwa tipe kelas yang digunakan daripada tipe antarmuka. EF tidak tahu tentang tipe antarmuka sehingga tidak dapat mengubahnya menjadi SQL. Dengan batasan kelas, tipe yang disimpulkan adalah tipe DbSet <T> yang mana EF tahu apa yang harus dilakukan.
jwize
2
Sempurna, sangat bagus bisa melakukan kueri berbasis Antarmuka dan tetap mempertahankan koleksi sebagai IQuerable. Agak menjengkelkan bahwa pada dasarnya tidak ada cara untuk memikirkan perbaikan ini, tanpa mengetahui cara kerja EF.
Anders
Apa yang Anda lihat di sini adalah batasan waktu kompilator yang memungkinkan kompilator C # untuk menentukan bahwa T adalah tipe IEntity dalam metode sehingga dapat menentukan bahwa setiap penggunaan "barang" IEntity valid karena selama waktu kompilasi kode MSIL dihasilkan akan melakukan pemeriksaan ini secara otomatis untuk Anda sebelum panggilan. Untuk memperjelas, menambahkan "class" sebagai batasan tipe di sini memungkinkan collection.FirstOrDefault () berjalan dengan benar karena kemungkinan mengembalikan instance baru T yang memanggil ctor default pada tipe berbasis kelas.
Perang
64

Beberapa penjelasan tambahan tentang class"perbaikan".

Jawaban ini menunjukkan dua ekspresi yang berbeda, satu dengan dan yang lainnya tanpa where T: classbatasan. Tanpa classkendala kami memiliki:

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

dan dengan kendala:

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

Kedua ekspresi ini diperlakukan secara berbeda oleh kerangka entitas. Melihat sumber EF 6 , seseorang dapat menemukan bahwa pengecualiannya berasal dari sini, lihatValidateAndAdjustCastTypes() .

Apa yang terjadi adalah, bahwa EF mencoba untuk IEntitymemasukkan sesuatu yang masuk akal di dunia model domain, namun gagal dalam melakukannya, oleh karena itu pengecualian dilemparkan.

Ekspresi dengan classbatasan tidak berisi Convert()operator, cast tidak dicoba dan semuanya baik-baik saja.

Masih menjadi pertanyaan terbuka, mengapa LINQ membangun ekspresi yang berbeda? Saya berharap beberapa penyihir C # bisa menjelaskan hal ini.

Tadej Mali
sumber
1
Terima kasih untuk penjelasannya.
Jace Rhea
9
@JonSkeet seseorang mencoba memanggil penyihir C # di sini. Kamu dimana
Nick N.
23

Entity Framework tidak mendukung ini di luar kotak, tetapi kerangka kerja ExpressionVisitoryang menerjemahkan ekspresi mudah ditulis:

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

Satu-satunya hal yang harus Anda lakukan adalah mengubah predikat yang diteruskan menggunakan ekspresi pengunjung sebagai berikut:

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

    ...
}

Pendekatan lain -less flexible- adalah dengan memanfaatkan 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
sumber
1

Saya mengalami kesalahan yang sama tetapi masalah yang serupa tetapi berbeda. Saya mencoba membuat fungsi ekstensi yang mengembalikan IQuer dapat tetapi kriteria filter didasarkan pada kelas dasar.

saya akhirnya menemukan solusi yang untuk metode ekstensi saya untuk memanggil. Pilih (e => e sebagai T) di mana T adalah kelas anak dan e adalah kelas dasar.

detail lengkapnya ada di sini: Buat ekstensi <T> IQuerable menggunakan kelas dasar di EF

Justin
sumber