Solusi 'Contains ()' menggunakan LINQ ke Entitas?

86

Saya mencoba membuat kueri yang menggunakan daftar id di klausa where, menggunakan api klien Silverlight ADO.Net Data Services (dan karenanya Linq To Entities). Adakah yang tahu tentang solusi untuk Mengandung tidak didukung?

Saya ingin melakukan sesuatu seperti ini:

List<long?> txnIds = new List<long?>();
// Fill list 

var q = from t in svc.OpenTransaction
        where txnIds.Contains(t.OpenTransactionId)
        select t;

Mencoba ini:

var q = from t in svc.OpenTransaction
where txnIds.Any<long>(tt => tt == t.OpenTransactionId)
select t;

Tapi mendapat "Metode 'Any' tidak didukung".

James Bloomer
sumber
36
Catatan: Entity Framework 4 (di .NET 4) memiliki metode "Berisi", kalau-kalau seseorang membaca ini dan tidak mengetahuinya. Saya tahu OP menggunakan EF1 (.NET 3.5).
DarrellNorton
7
@Darrell Saya hanya menyia-nyiakan setengah jam karena saya melewatkan komentar Anda. Saya berharap saya dapat membuat komentar Anda berkedip dan marquee di layar.
Chris Dwyer

Jawaban:

97

Pembaruan: EF ≥ 4 mendukung Containssecara langsung (Checkout Any), jadi Anda tidak memerlukan solusi apa pun.

public static IQueryable<TEntity> WhereIn<TEntity, TValue>
  (
    this ObjectQuery<TEntity> query,
    Expression<Func<TEntity, TValue>> selector,
    IEnumerable<TValue> collection
  )
{
  if (selector == null) throw new ArgumentNullException("selector");
  if (collection == null) throw new ArgumentNullException("collection");
  if (!collection.Any()) 
    return query.Where(t => false);

  ParameterExpression p = selector.Parameters.Single();

  IEnumerable<Expression> equals = collection.Select(value =>
     (Expression)Expression.Equal(selector.Body,
          Expression.Constant(value, typeof(TValue))));

  Expression body = equals.Aggregate((accumulate, equal) =>
      Expression.Or(accumulate, equal));

  return query.Where(Expression.Lambda<Func<TEntity, bool>>(body, p));
}

//Optional - to allow static collection:
public static IQueryable<TEntity> WhereIn<TEntity, TValue>
  (
    this ObjectQuery<TEntity> query,
    Expression<Func<TEntity, TValue>> selector,
    params TValue[] collection
  )
{
  return WhereIn(query, selector, (IEnumerable<TValue>)collection);
}

PEMAKAIAN:

public static void Main()
{
  using (MyObjectContext context = new MyObjectContext())
  {
    //Using method 1 - collection provided as collection
    var contacts1 =
      context.Contacts.WhereIn(c => c.Name, GetContactNames());

    //Using method 2 - collection provided statically
    var contacts2 = context.Contacts.WhereIn(c => c.Name,
      "Contact1",
      "Contact2",
      "Contact3",
      "Contact4"
      );
  }
}
Shimmy Weitzhandler
sumber
6
Peringatan; ketika arg adalah koleksi besar (milik saya 8500 item daftar int), stack overflow. Anda mungkin berpikir itu gila untuk melewatkan daftar seperti itu, tetapi saya pikir ini menunjukkan kelemahan dalam pendekatan ini.
dudeNumber4
2
Koreksi saya jika saya salah. tetapi ini berarti ketika collection (filter) yang diteruskan adalah himpunan kosong, pada dasarnya akan menghasilkan semua data karena itu hanya mengembalikan parameter kueri. Saya mengharapkannya untuk memfilter semua nilai, apakah ada cara untuk melakukan ini?
Tidur siang tanggal
1
Jika Anda bermaksud bahwa saat pemeriksaan koleksi kosong, seharusnya tidak ada hasil, dalam cuplikan di atas if (!collection.Any()) //action;, ganti tindakan - ganti dengan hanya mengembalikan kueri kosong dari jenis yang diminta untuk kinerja terbaik - atau hapus saja baris ini.
Shimmy Weitzhandler
1
return WhereIn (query, selector, collection); harus diganti dengan return WhereIn (query, selector, (IEnumerable <TValue>) collection); untuk menghindari rekursi yang tidak diinginkan.
Antoine Aubry
1
Saya yakin ada bug di kode. Jika daftar nilai yang diberikan kosong, perilaku yang benar adalah tidak mengembalikan hasil - yaitu / tidak ada objek dalam kueri yang ada dalam koleksi. Namun, kode melakukan kebalikannya - semua nilai dikembalikan, bukan tidak satu pun. Saya yakin Anda ingin "if (! Collection.Any ()) return query.Where (e => false)"
ShadowChaser
18

Anda dapat kembali ke tangan coding beberapa e-sql (perhatikan kata kunci "it"):

return CurrentDataSource.Product.Where("it.ID IN {4,5,6}"); 

Berikut adalah kode yang saya gunakan untuk menghasilkan beberapa e-sql dari koleksi, YMMV:

string[] ids = orders.Select(x=>x.ProductID.ToString()).ToArray();
return CurrentDataSource.Products.Where("it.ID IN {" + string.Join(",", ids) + "}");
Rob Fonseca-Ensor
sumber
1
Apakah Anda memiliki info lebih lanjut tentang "itu"? Awalan "it" muncul di sampel MSDN, tetapi saya tidak dapat menemukan penjelasan tentang kapan / mengapa "itu" diperlukan.
Robert Claypool
1
Digunakan dalam kueri dinamis Entity Framework, lihat geekswithblogs.net/thanigai/archive/2009/04/29/… , Thanigainathan Siranjeevi menjelaskannya di sana.
Shimmy Weitzhandler
13

Dari MSDN :

static Expression<Func<TElement, bool>> BuildContainsExpression<TElement, TValue>(
    Expression<Func<TElement, TValue>> valueSelector, IEnumerable<TValue> values)
{
    if (null == valueSelector) { throw new ArgumentNullException("valueSelector"); }
    if (null == values) { throw new ArgumentNullException("values"); }
    ParameterExpression p = valueSelector.Parameters.Single();

    // p => valueSelector(p) == values[0] || valueSelector(p) == ...
    if (!values.Any())
    {
        return e => false;
    }

    var equals = values.Select(
             value => (Expression)Expression.Equal(valueSelector.Body, Expression.Constant(value, typeof(TValue))));

    var body = equals.Aggregate<Expression>((accumulate, equal) => Expression.Or(accumulate, equal));

    return Expression.Lambda<Func<TElement, bool>>(body, p);
} 

dan kueri menjadi:

var query2 = context.Entities.Where(BuildContainsExpression<Entity, int>(e => e.ID, ids));
James Bloomer
sumber
3
Jika Anda ingin melakukan 'Not contains', cukup lakukan pengeditan berikut dalam metode BuildContainsExpression: - Expression.Equal menjadi Expression.NotEqual - Expression.Or menjadi Expression.And
Merritt
2

Saya tidak yakin tentang Silverligth, tetapi di linq ke objek saya selalu menggunakan any () untuk kueri ini.

var q = from t in svc.OpenTranaction
        where txnIds.Any(t.OpenTransactionId)
        select t;
AndreasN
sumber
5
Any tidak mengambil objek dari jenis urutan - itu tidak memiliki parameter (dalam hal ini hanya "apakah ini kosong atau tidak") atau mengambil predikat.
Jon Skeet
Saya sangat senang menemukan jawaban ini :) +1 Terima kasih AndreasN
SDReyes
1

Untuk melengkapi rekaman, inilah kode yang akhirnya saya gunakan (pengecekan kesalahan dihilangkan untuk kejelasan) ...

// How the function is called
var q = (from t in svc.OpenTransaction.Expand("Currency,LineItem")
         select t)
         .Where(BuildContainsExpression<OpenTransaction, long>(tt => tt.OpenTransactionId, txnIds));



 // The function to build the contains expression
   static System.Linq.Expressions.Expression<Func<TElement, bool>> BuildContainsExpression<TElement, TValue>(
                System.Linq.Expressions.Expression<Func<TElement, TValue>> valueSelector, 
                IEnumerable<TValue> values)
        {
            if (null == valueSelector) { throw new ArgumentNullException("valueSelector"); }
            if (null == values) { throw new ArgumentNullException("values"); }
            System.Linq.Expressions.ParameterExpression p = valueSelector.Parameters.Single();

            // p => valueSelector(p) == values[0] || valueSelector(p) == ...
            if (!values.Any())
            {
                return e => false;
            }

            var equals = values.Select(value => (System.Linq.Expressions.Expression)System.Linq.Expressions.Expression.Equal(valueSelector.Body, System.Linq.Expressions.Expression.Constant(value, typeof(TValue))));
            var body = equals.Aggregate<System.Linq.Expressions.Expression>((accumulate, equal) => System.Linq.Expressions.Expression.Or(accumulate, equal));
            return System.Linq.Expressions.Expression.Lambda<Func<TElement, bool>>(body, p);
        }
James Bloomer
sumber
0

Terima kasih banyak. Metode ekstensi WhereIn sudah cukup bagi saya. Saya membuat profil dan menghasilkan perintah SQL yang sama ke DataBase sebagai e-sql.

public Estado[] GetSomeOtherMore(int[] values)
{
    var result = _context.Estados.WhereIn(args => args.Id, values) ;
    return result.ToArray();
}

Hasilkan ini:

SELECT 
[Extent1].[intIdFRLEstado] AS [intIdFRLEstado], 
[Extent1].[varDescripcion] AS [varDescripcion]
FROM [dbo].[PVN_FRLEstados] AS [Extent1]
WHERE (2 = [Extent1].[intIdFRLEstado]) OR (4 = [Extent1].[intIdFRLEstado]) OR (8 = [Extent1].[intIdFRLEstado])
jrojo
sumber
0

Maaf pengguna baru, saya akan mengomentari jawaban yang sebenarnya, tetapi sepertinya saya belum bisa melakukannya?

Bagaimanapun, sehubungan dengan jawaban dengan kode sampel untuk BuildContainsExpression (), ketahuilah bahwa jika Anda menggunakan metode itu pada Entitas database (yaitu bukan objek dalam memori) dan Anda menggunakan IQuerable, itu sebenarnya harus pergi ke database karena pada dasarnya ia melakukan banyak SQL "atau" kondisi untuk memeriksa klausa "where in" (jalankan dengan SQL Profiler untuk melihat).

Ini bisa berarti, jika Anda menyempurnakan IQuer dapat dengan beberapa BuildContainsExpression (), itu tidak akan mengubahnya menjadi satu pernyataan SQL yang dijalankan di akhir seperti yang Anda harapkan.

Solusi bagi kami adalah menggunakan beberapa gabungan LINQ untuk menyimpannya ke satu panggilan SQL.

Shannon
sumber
0

Selain jawaban yang dipilih.

Ganti Expression.Ordengan Expression.OrElseuntuk digunakan dengan Nhibernate dan perbaiki Unable to cast object of type 'NHibernate.Hql.Ast.HqlBitwiseOr' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'pengecualian.

smg
sumber