Bagikan melalui


Kueri berdasarkan status run-time

Di sebagian besar kueri LINQ, bentuk umum kueri diatur dalam kode. Anda dapat memfilter item menggunakan where klausa, mengurutkan koleksi output menggunakan orderby, item grup, atau melakukan beberapa komputasi. Kode Anda mungkin menyediakan parameter untuk filter, atau kunci pengurutan, atau ekspresi lain yang merupakan bagian dari kueri. Namun, bentuk keseluruhan kueri tidak dapat berubah. Dalam artikel ini, Anda mempelajari teknik untuk menggunakan System.Linq.IQueryable<T> antarmuka dan jenis yang mengimplementasikannya untuk memodifikasi bentuk kueri pada waktu proses.

Anda menggunakan teknik ini untuk membuat kueri pada waktu proses, di mana beberapa input pengguna atau status run-time mengubah metode kueri yang ingin Anda gunakan sebagai bagian dari kueri. Anda ingin mengedit kueri dengan menambahkan, menghapus, atau mengubah klausa kueri.

Catatan

Pastikan Anda menambahkan using System.Linq.Expressions; dan using static System.Linq.Expressions.Expression; di bagian atas file .cs Anda.

Pertimbangkan kode yang mendefinisikan IQueryable atau terhadap IQueryable<T> sumber data:

string[] companyNames = [
    "Consolidated Messenger", "Alpine Ski House", "Southridge Video",
    "City Power & Light", "Coho Winery", "Wide World Importers",
    "Graphic Design Institute", "Adventure Works", "Humongous Insurance",
    "Woodgrove Bank", "Margie's Travel", "Northwind Traders",
    "Blue Yonder Airlines", "Trey Research", "The Phone Company",
    "Wingtip Toys", "Lucerne Publishing", "Fourth Coffee"
];

// Use an in-memory array as the data source, but the IQueryable could have come
// from anywhere -- an ORM backed by a database, a web request, or any other LINQ provider.
IQueryable<string> companyNamesSource = companyNames.AsQueryable();
var fixedQry = companyNames.OrderBy(x => x);

Setiap kali Anda menjalankan kode sebelumnya, kueri yang sama persis dijalankan. Mari kita pelajari cara mengubah kueri memperluasnya atau mengubahnya. Pada dasarnya, sebuah IQueryable memiliki dua komponen:

  • Expression—representasi agnostik bahasa dan sumber data-agnostik dari komponen kueri saat ini, dalam bentuk pohon ekspresi.
  • Provider—contoh penyedia LINQ yang mengetahui cara mewujudkan kueri saat ini menjadi nilai atau serangkaian nilai.

Dalam konteks kueri dinamis, penyedia biasanya tetap sama; pohon ekspresi kueri berbeda dari kueri ke kueri.

Pohon ekspresi tidak dapat diubah; jika Anda menginginkan pohon ekspresi yang berbeda—dan dengan demikian kueri yang berbeda—Anda perlu menerjemahkan pohon ekspresi yang ada ke yang baru. Bagian berikut ini menjelaskan teknik khusus untuk mengkueri secara berbeda sebagai respons terhadap status run-time:

  • Menggunakan status run-time dari dalam pohon ekspresi
  • Memanggil metode LINQ lainnya
  • Memvariasikan pohon ekspresi yang diteruskan ke metode LINQ
  • Membuat Expression<TDelegate> pohon ekspresi menggunakan metode pabrik di Expression
  • Menambahkan node panggilan metode ke pohon ekspresi IQueryable
  • Membuat string dan menggunakan pustaka Dynamic LINQ

Masing-masing teknik memungkinkan lebih banyak kemampuan, tetapi dengan biaya kompleksitas yang meningkat.

Menggunakan status run-time dari dalam pohon ekspresi

Cara paling sederhana untuk mengkueri secara dinamis adalah dengan mereferensikan status run-time langsung dalam kueri melalui variabel tertutup, seperti length dalam contoh kode berikut:

var length = 1;
var qry = companyNamesSource
    .Select(x => x.Substring(0, length))
    .Distinct();

Console.WriteLine(string.Join(",", qry));
// prints: C, A, S, W, G, H, M, N, B, T, L, F

length = 2;
Console.WriteLine(string.Join(",", qry));
// prints: Co, Al, So, Ci, Wi, Gr, Ad, Hu, Wo, Ma, No, Bl, Tr, Th, Lu, Fo

Pohon ekspresi internal—dan dengan demikian kueri—tidak dimodifikasi; kueri mengembalikan nilai yang berbeda hanya karena nilai length diubah.

Memanggil metode LINQ lainnya

Umumnya, metode LINQ bawaan di Queryable melakukan dua langkah:

Anda bisa mengganti kueri asli dengan hasil System.Linq.IQueryable<T>metode -returning, untuk mendapatkan kueri baru. Anda dapat menggunakan status run-time, seperti dalam contoh berikut:

// bool sortByLength = /* ... */;

var qry = companyNamesSource;
if (sortByLength)
{
    qry = qry.OrderBy(x => x.Length);
}

Memvariasikan pohon ekspresi yang diteruskan ke metode LINQ

Anda dapat meneruskan ekspresi yang berbeda ke metode LINQ, tergantung pada status run-time:

// string? startsWith = /* ... */;
// string? endsWith = /* ... */;

Expression<Func<string, bool>> expr = (startsWith, endsWith) switch
{
    ("" or null, "" or null) => x => true,
    (_, "" or null) => x => x.StartsWith(startsWith),
    ("" or null, _) => x => x.EndsWith(endsWith),
    (_, _) => x => x.StartsWith(startsWith) || x.EndsWith(endsWith)
};

var qry = companyNamesSource.Where(expr);

Anda mungkin juga ingin menyusun berbagai subekspresi menggunakan pustaka lain seperti PredicateBuilder LinqKit:

// This is functionally equivalent to the previous example.

// using LinqKit;
// string? startsWith = /* ... */;
// string? endsWith = /* ... */;

Expression<Func<string, bool>>? expr = PredicateBuilder.New<string>(false);
var original = expr;
if (!string.IsNullOrEmpty(startsWith))
{
    expr = expr.Or(x => x.StartsWith(startsWith));
}
if (!string.IsNullOrEmpty(endsWith))
{
    expr = expr.Or(x => x.EndsWith(endsWith));
}
if (expr == original)
{
    expr = x => true;
}

var qry = companyNamesSource.Where(expr);

Membangun pohon-pohon dan kueri ekspresi menggunakan metode pabrik

Dalam semua contoh hingga titik ini, Anda mengetahui jenis elemen pada waktu kompilasi—string—dan dengan demikian jenis kueri—IQueryable<string>. Anda dapat menambahkan komponen ke kueri jenis elemen apa pun, atau untuk menambahkan komponen yang berbeda, tergantung pada jenis elemen. Anda dapat membuat pohon-pohon ekspresi secara lengkap menggunakan metode pabrik di System.Linq.Expressions.Expression, dan dengan demikian menyesuaikan ekspresi saat dijalankan ke jenis elemen tertentu.

Membuat Ekspresi<TDelegate>

Saat Anda membuat ekspresi untuk meneruskan ke salah satu metode LINQ, Anda benar-benar membuat instans System.Linq.Expressions.Expression<TDelegate>, di mana TDelegate adalah beberapa jenis delegasi seperti Func<string, bool>, , Actionatau jenis delegasi kustom.

System.Linq.Expressions.Expression<TDelegate> mewarisi dari LambdaExpression, yang mewakili ekspresi lambda lengkap seperti contoh berikut:

Expression<Func<string, bool>> expr = x => x.StartsWith("a");

Sebuah LambdaExpression memiliki dua komponen:

  1. Daftar parameter—(string x)—diwakili oleh properti Parameters.
  2. Isi—x.StartsWith("a")—diwakili oleh properti Body.

Langkah-langkah dasar dalam membangun adalah Expression<TDelegate> sebagai berikut:

  1. Tentukan objek ParameterExpression untuk setiap parameter (jika ada) di dalam ekspresi lambda menggunakan metode pabrik Parameter.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. Buat tubuh Anda LambdaExpression, menggunakan metode yang ParameterExpression ditentukan, dan pabrik di Expression. Misalnya, ekspresi yang mewakili x.StartsWith("a") dapat dibuat seperti berikut:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Membungkus parameter dan isi dalam Ekspresi<TDelegate> jenis waktu kompilasi dengan menggunakan kelebihan beban metode pabrik Lambda yang sesuai:
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

Bagian berikut menjelaskan skenario di mana Anda mungkin ingin membuat Expression<TDelegate> untuk meneruskan ke metode LINQ. Ini memberikan contoh lengkap tentang cara melakukannya menggunakan metode pabrik.

Membuat kueri penuh pada waktu proses

Anda ingin menulis kueri yang berfungsi dengan beberapa jenis entitas:

record Person(string LastName, string FirstName, DateTime DateOfBirth);
record Car(string Model, int Year);

Untuk setiap jenis entitas ini, Anda ingin memfilter dan mengembalikan entitas yang memiliki teks tertentu di dalam salah satu bidang string saja. Untuk Person, Anda ingin mencari properti FirstName dan LastName:

string term = /* ... */;
var personsQry = new List<Person>()
    .AsQueryable()
    .Where(x => x.FirstName.Contains(term) || x.LastName.Contains(term));

Tetapi untuk Car, Anda ingin mencari properti Model saja:

string term = /* ... */;
var carsQry = new List<Car>()
    .AsQueryable()
    .Where(x => x.Model.Contains(term));

Meskipun Anda dapat menulis satu fungsi kustom untuk IQueryable<Person> dan fungsi lainnya untuk IQueryable<Car>, fungsi berikut menambahkan pemfilteran ini ke kueri yang ada, terlepas dari jenis elemen tertentu.

// using static System.Linq.Expressions.Expression;

IQueryable<T> TextFilter<T>(IQueryable<T> source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }

    // T is a compile-time placeholder for the element type of the query.
    Type elementType = typeof(T);

    // Get all the string properties on this specific type.
    PropertyInfo[] stringProperties = elementType
        .GetProperties()
        .Where(x => x.PropertyType == typeof(string))
        .ToArray();
    if (!stringProperties.Any()) { return source; }

    // Get the right overload of String.Contains
    MethodInfo containsMethod = typeof(string).GetMethod("Contains", [typeof(string)])!;

    // Create a parameter for the expression tree:
    // the 'x' in 'x => x.PropertyName.Contains("term")'
    // The type of this parameter is the query's element type
    ParameterExpression prm = Parameter(elementType);

    // Map each property to an expression tree node
    IEnumerable<Expression> expressions = stringProperties
        .Select(prp =>
            // For each property, we have to construct an expression tree node like x.PropertyName.Contains("term")
            Call(                  // .Contains(...) 
                Property(          // .PropertyName
                    prm,           // x 
                    prp
                ),
                containsMethod,
                Constant(term)     // "term" 
            )
        );

    // Combine all the resultant expression nodes using ||
    Expression body = expressions
        .Aggregate((prev, current) => Or(prev, current));

    // Wrap the expression body in a compile-time-typed lambda expression
    Expression<Func<T, bool>> lambda = Lambda<Func<T, bool>>(body, prm);

    // Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the Where method
    return source.Where(lambda);
}

TextFilter Karena fungsi mengambil dan mengembalikan IQueryable<T> (dan bukan hanya ), IQueryableAnda dapat menambahkan elemen kueri kompilasi-waktu-ketik lebih lanjut setelah filter teks.

var qry = TextFilter(
        new List<Person>().AsQueryable(),
        "abcd"
    )
    .Where(x => x.DateOfBirth < new DateTime(2001, 1, 1));

var qry1 = TextFilter(
        new List<Car>().AsQueryable(),
        "abcd"
    )
    .Where(x => x.Year == 2010);

Menambahkan node panggilan metode ke pohon ekspresi IQueryable<TDelegate>

Jika Anda memiliki IQueryable alih-alih IQueryable<T>, Anda tidak dapat langsung memanggil metode LINQ generik. Salah satu alternatifnya adalah membangun pohon ekspresi dalam seperti yang ditunjukkan pada contoh sebelumnya, dan menggunakan pantulan untuk memanggil metode LINQ yang sesuai saat melewati di pohon ekspresi.

Anda juga dapat menduplikasi fungsionalitas metode LINQ, dengan membungkus seluruh pohon dalam MethodCallExpression yang mewakili panggilan ke metode LINQ:

IQueryable TextFilter_Untyped(IQueryable source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }
    Type elementType = source.ElementType;

    // The logic for building the ParameterExpression and the LambdaExpression's body is the same as in the previous example,
    // but has been refactored into the constructBody function.
    (Expression? body, ParameterExpression? prm) = constructBody(elementType, term);
    if (body is null) { return source; }

    Expression filteredTree = Call(
        typeof(Queryable),
        "Where",
        [elementType],
        source.Expression,
        Lambda(body, prm!)
    );

    return source.Provider.CreateQuery(filteredTree);
}

Dalam hal ini, Anda tidak memiliki tempat penampung generik waktu kompilasi, sehingga Anda menggunakan kelebihan beban yang tidak memerlukan informasi jenis waktu kompilasi, dan yang menghasilkan LambdaExpression alih-alih TExpression<TDelegate>.Lambda

Pustaka Dynamic LINQ

Membangun pohon ekspresi menggunakan metode pabrik relatif kompleks; lebih mudah untuk menyusun string. Pustaka Dynamic LINQ memaparkan serangkaian metode ekstensi pada IQueryable yang sesuai dengan metode LINQ standar di Queryable, dan yang menerima string dalam sintaks khusus alih-alih pohon-pohon ekspresi. Pustaka menghasilkan pohon ekspresi yang sesuai dari string, dan dapat mengembalikan hasil yang diterjemahkan IQueryable.

Misalnya, contoh sebelumnya dapat ditulis ulang sebagai berikut:

// using System.Linq.Dynamic.Core

IQueryable TextFilter_Strings(IQueryable source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }

    var elementType = source.ElementType;

    // Get all the string property names on this specific type.
    var stringProperties =
        elementType.GetProperties()
            .Where(x => x.PropertyType == typeof(string))
            .ToArray();
    if (!stringProperties.Any()) { return source; }

    // Build the string expression
    string filterExpr = string.Join(
        " || ",
        stringProperties.Select(prp => $"{prp.Name}.Contains(@0)")
    );

    return source.Where(filterExpr, term);
}