Bagikan melalui


Kueri berdasarkan status runtime (Visual Basic)

Pertimbangkan kode yang mendefinisikan IQueryable atau IQueryable(Of T) terhadap sumber data:

Dim companyNames As String() = {
    "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"
}

' We're using 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.
Dim companyNamesSource As IQueryable(Of String) = companyNames.AsQueryable
Dim fixedQry = companyNamesSource.OrderBy(Function(x) x)

Setiap kali Anda menjalankan kode ini, kueri yang sama persis akan dijalankan. Ini sering tidak terlalu berguna, karena Anda mungkin ingin kode Anda menjalankan kueri yang berbeda tergantung pada kondisi pada waktu proses. Artikel ini menjelaskan cara menjalankan kueri yang berbeda berdasarkan status runtime.

IQueryable / IQueryable(Of T) dan pohon ekspresi

IQueryable pada dasarnya memiliki dua komponen:

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

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

Pohon ekspresi tidak dapat diubah; jika Anda menginginkan pohon ekspresi yang berbeda—dan dengan demikian kueri yang berbeda—Anda harus menerjemahkan pohon ekspresi yang ada ke yang baru, dan dengan demikian ke yang baru IQueryable.

Bagian berikut ini menjelaskan teknik khusus untuk mengkueri secara berbeda sebagai respons terhadap status runtime:

  • Gunakan status runtime di dalam pohon ekspresi
  • Memanggil metode LINQ tambahan
  • Memvariasikan pohon ekspresi yang diteruskan ke metode LINQ
  • Bangun pohon ekspresi Ekspresi (Dari TDelegate) dengan menggunakan metode pabrik di Expression
  • Menambahkan node pemanggilan metode ke pohon ekspresi IQueryable
  • Buat string, dan gunakan pustaka Dynamic LINQ

Gunakan status runtime di dalam pohon ekspresi

Dengan asumsi penyedia LINQ mendukungnya, cara paling sederhana untuk mengkueri secara dinamis adalah dengan mereferensikan status runtime langsung dalam kueri melalui variabel tertutup, seperti length dalam contoh kode berikut:

Dim length = 1
Dim qry = companyNamesSource.
    Select(Function(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—belum dimodifikasi; kueri mengembalikan nilai yang berbeda hanya karena nilai length telah diubah.

Memanggil metode LINQ tambahan

Umumnya, metode LINQ bawaan melakukan Queryable dua langkah:

Anda dapat mengganti kueri asli dengan hasil dari metode yang mengembalikan IQueryable(Of T), untuk mendapatkan kueri baru. Anda dapat melakukan ini secara kondisional berdasarkan status runtime, seperti dalam contoh berikut:

' Dim sortByLength As Boolean  = ...

Dim qry = companyNamesSource
If sortByLength Then qry = qry.OrderBy(Function(x) x.Length)

Memvariasikan pohon ekspresi yang diteruskan ke metode LINQ

Anda dapat meneruskan ekspresi yang berbeda ke metode LINQ, tergantung pada status runtime:

' Dim startsWith As String = ...
' Dim endsWith As String = ...

Dim expr As Expression(Of Func(Of String, Boolean))
If String.IsNullOrEmpty(startsWith) AndAlso String.IsNullOrEmpty(endsWith) Then
    expr = Function(x) True
ElseIf String.IsNullOrEmpty(startsWith) Then
    expr = Function(x) x.EndsWith(endsWith)
ElseIf String.IsNullOrEmpty(endsWith) Then
    expr = Function(x) x.StartsWith(startsWith)
Else
    expr = Function(x) x.StartsWith(startsWith) AndAlso x.EndsWith(endsWith)
End If
Dim qry = companyNamesSource.Where(expr)

Anda mungkin juga ingin menyusun berbagai subekspresi menggunakan pustaka pihak ketiga seperti PredicateBuilderLinqKit:

' This is functionally equivalent to the previous example.

' Imports LinqKit
' Dim startsWith As String = ...
' Dim endsWith As String = ...

Dim expr As Expression(Of Func(Of String, Boolean)) = PredicateBuilder.[New](Of String)(False)
Dim original = expr
If Not String.IsNullOrEmpty(startsWith) Then expr = expr.Or(Function(x) x.StartsWith(startsWith))
If Not String.IsNullOrEmpty(endsWith) Then expr = expr.Or(Function(x) x.EndsWith(endsWith))
If expr Is original Then expr = Function(x) True

Dim qry = companyNamesSource.Where(expr)

Membuat pohon ekspresi dan kueri menggunakan metode fabrikasi

Dalam semua contoh hingga saat ini, kita telah mengetahui jenis elemen pada waktu kompilasi—String—dan dengan demikian jenis kueri—IQueryable(Of String). Anda mungkin perlu menambahkan komponen ke kueri jenis elemen apa pun, atau untuk menambahkan komponen yang berbeda tergantung pada jenis elemen. Anda dapat membuat pohon ekspresi dari awal, menggunakan metode bawaan di System.Linq.Expressions.Expression, dan dengan demikian menyesuaikan ekspresi saat runtime dengan jenis elemen tertentu.

Membangun Ekspresi (Dari TDelegate)

Saat Anda membuat ekspresi untuk meneruskan ke salah satu metode LINQ, Anda benar-benar membuat instans Ekspresi (Dari TDelegate), di mana TDelegate adalah beberapa jenis delegasi seperti Func(Of String, Boolean), , Actionatau jenis delegasi kustom.

Ekspresi(Dari TDelegate) mewarisi dari LambdaExpression, yang mewakili ekspresi lambda lengkap seperti berikut ini:

Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")

A LambdaExpression memiliki dua komponen:

  • Daftar parameter—(x As String)—diwakili oleh Parameters properti .
  • Sebuah elemen—x.StartsWith("a")—diwakili oleh properti Body.

Langkah-langkah dasar dalam membangun Ekspresi (Dari TDelegate) adalah sebagai berikut:

  • Tentukan objek ParameterExpression untuk setiap parameter (jika ada) dalam ekspresi lambda, dengan menggunakan metode pabrikasi Parameter.

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • Buat kerangka LambdaExpression Anda, menggunakan ParameterExpression(s) yang telah Anda tentukan, dan metode pabrik di Expression. Misalnya, ekspresi yang mewakili x.StartsWith("a") dapat dibangun seperti ini:

    Dim body As Expression = [Call](
        x,
        GetType(String).GetMethod("StartsWith", {GetType(String)}),
        Constant("a")
    )
    
  • Bungkus parameter dan isi dalam Expression(Of TDelegate) yang bertipe waktu kompilasi, menggunakan kelebihan beban metode pabrik yang sesuai Lambda.

    Dim expr As Expression(Of Func(Of String, Boolean)) =
        Lambda(Of Func(Of String, Boolean))(body, x)
    

Bagian berikut menjelaskan skenario di mana Anda mungkin ingin membuat Ekspresi (Dari TDelegate) untuk diteruskan ke metode LINQ, dan memberikan contoh lengkap tentang cara melakukannya menggunakan metode pabrik.

Skenario

Katakanlah Anda memiliki beberapa jenis entitas:

Public Class Person
    Property LastName As String
    Property FirstName As String
    Property DateOfBirth As Date
End Class

Public Class Car
    Property Model As String
    Property Year As Integer
End Class

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

' Dim term = ...
Dim personsQry = (New List(Of Person)).AsQueryable.
    Where(Function(x) x.FirstName.Contains(term) OrElse x.LastName.Contains(term))

Tetapi untuk Car, Anda hanya ingin mencari properti Model.

' Dim term = ...
Dim carsQry = (New List(Of Car)).AsQueryable.
    Where(Function(x) x.Model.Contains(term))

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

Contoh

' Imports System.Linq.Expressions.Expression
Function TextFilter(Of T)(source As IQueryable(Of T), term As String) As IQueryable(Of T)
    If String.IsNullOrEmpty(term) Then Return source

    ' T is a compile-time placeholder for the element type of the query
    Dim elementType = GetType(T)

    ' Get all the string properties on this specific type
    Dim stringProperties As PropertyInfo() =
        elementType.GetProperties.
            Where(Function(x) x.PropertyType = GetType(String)).
            ToArray
    If stringProperties.Length = 0 Then Return source

    ' Get the right overload of String.Contains
    Dim containsMethod As MethodInfo =
        GetType(String).GetMethod("Contains", {GetType(String)})

    ' Create the parameter for the expression tree --
    ' the 'x' in 'Function(x) x.PropertyName.Contains("term")'
    ' The type of the parameter is the query's element type
    Dim prm As ParameterExpression =
        Parameter(elementType)

    ' Generate an expression tree node corresponding to each property
    Dim expressions As IEnumerable(Of Expression) =
        stringProperties.Select(Of Expression)(Function(prp)
                                                   ' For each property, we want an expression node like this:
                                                   ' x.PropertyName.Contains("term")
                                                   Return [Call](      ' .Contains(...)
                                                       [Property](     ' .PropertyName
                                                           prm,        ' x
                                                           prp
                                                       ),
                                                       containsMethod,
                                                       Constant(term)  ' "term"
                                                   )
                                               End Function)

    ' Combine the individual nodes into a single expression tree node using OrElse
    Dim body As Expression =
        expressions.Aggregate(Function(prev, current) [OrElse](prev, current))

    ' Wrap the expression body in a compile-time-typed lambda expression
    Dim lmbd As Expression(Of Func(Of T, Boolean)) =
        Lambda(Of Func(Of T, Boolean))(body, prm)

    ' Because the lambda is compile-time-typed, we can use it with the Where method
    Return source.Where(lmbd)
End Function

TextFilter Karena fungsi ini mengambil dan mengembalikan IQueryable(Of T) (dan bukan hanya IQueryable), Anda dapat menambahkan elemen kueri yang bertipe waktu kompilasi lebih lanjut setelah filter teks.

Dim qry = TextFilter(
    (New List(Of Person)).AsQueryable,
    "abcd"
).Where(Function(x) x.DateOfBirth < #1/1/2001#)

Dim qry1 = TextFilter(
    (New List(Of Car)).AsQueryable,
    "abcd"
).Where(Function(x) x.Year = 2010)

Tambahkan node panggilan metode ke IQueryable pohon ekspresi

Jika Anda memiliki IQueryable alih-alih IQueryable(Of T), Anda tidak dapat langsung memanggil metode LINQ generik. Salah satu alternatifnya adalah membangun pohon ekspresi dalam seperti di atas, dan menggunakan pantulan untuk memanggil metode LINQ yang sesuai saat melewati pohon ekspresi.

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

Function TextFilter_Untyped(source As IQueryable, term As String) As IQueryable
    If String.IsNullOrEmpty(term) Then Return source
    Dim 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.
    Dim x As (Expression, ParameterExpression) = ConstructBody(elementType, term)
    Dim body As Expression = x.Item1
    Dim prm As ParameterExpression = x.Item2
    If body Is Nothing Then Return source

    Dim filteredTree As Expression = [Call](
        GetType(Queryable),
        "Where",
        {elementType},
        source.Expression,
        Lambda(body, prm)
    )

    Return source.Provider.CreateQuery(filteredTree)
End Function

Dalam hal ini, Anda tidak memiliki tempat penampung generik waktu kompilasi T, sehingga Anda akan menggunakan Lambda overload yang tidak memerlukan informasi jenis waktu kompilasi, dan yang menghasilkan LambdaExpression, bukan Ekspresi(Of TDelegate).

Pustaka Dynamic LINQ

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

Misalnya, contoh sebelumnya (termasuk konstruksi pohon ekspresi) dapat ditulis ulang sebagai berikut:

' Imports System.Linq.Dynamic.Core

Function TextFilter_Strings(source As IQueryable, term As String) As IQueryable
    If String.IsNullOrEmpty(term) Then Return source

    Dim elementType = source.ElementType
    Dim stringProperties = elementType.GetProperties.
            Where(Function(x) x.PropertyType = GetType(String)).
            ToArray
    If stringProperties.Length = 0 Then Return source

    Dim filterExpr = String.Join(
        " || ",
        stringProperties.Select(Function(prp) $"{prp.Name}.Contains(@0)")
    )

    Return source.Where(filterExpr, term)
End Function

Lihat juga