다음을 통해 공유


런타임 상태에 따라 쿼리(Visual Basic)

데이터 소스에 대해 IQueryable 또는 IQueryable(Of T)을 정의하는 코드를 고려합니다.

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)

해당 코드를 실행할 때마다 똑같은 쿼리가 실행됩니다. 코드가 런타임에 조건에 따라 서로 다른 쿼리를 실행하도록 해야 할 수 있으므로 이 방식은 그다지 유용하지 않습니다. 이 문서에서는 런타임 상태에 따라 다른 쿼리를 실행하는 방법을 설명합니다.

IQueryable/IQueryable(Of T) 및 식 트리

기본적으로 IQueryable에는 다음 두 가지 구성 요소가 있습니다.

  • Expression식 트리 형식으로 현재 쿼리 구성 요소의 언어 및 데이터 소스 중립적 표현입니다.
  • Provider현재 쿼리를 값 또는 값 세트로 구체화하는 방법을 인식하는 LINQ 공급자의 인스턴스입니다.

동적 쿼리 컨텍스트에서 공급자는 일반적으로 동일하게 유지됩니다. 쿼리의 식 트리는 쿼리별로 다릅니다.

식 트리는 변경할 수 없습니다. 다른 식 트리 및 이에 따라 다른 쿼리가 필요한 경우에는 기존 식 트리를 새 식 트리로 변환하고 이에 따라 새 IQueryable(으)로 변환해야 합니다.

다음 섹션에서는 런타임 상태에 대한 응답으로 다르게 쿼리하는 특정 기술을 설명합니다.

식 트리 내에서 런타임 상태 사용

LINQ 공급자가 지원하는 경우 동적으로 쿼리하는 가장 간단한 방법은 다음 코드 예제의 length 같이 둘러싸인 변수를 통해 쿼리에서 직접 런타임 상태를 참조하는 것입니다.

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

내부 식 트리 및 이에 따른 쿼리는 수정되지 않았습니다. length 값이 변경되었기 때문에 쿼리는 다른 값을 반환합니다.

추가 LINQ 메서드 호출

일반적으로 Queryable기본 제공 LINQ 메서드는 다음 두 단계를 수행합니다.

원래 쿼리를 IQueryable(Of T) 반환 메서드의 결과로 바꿔서 새 쿼리를 가져올 수 있습니다. 다음 예제와 같이 런타임 상태에 따라 해당 작업을 조건부로 수행할 수 있습니다.

' Dim sortByLength As Boolean  = ...

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

LINQ 메서드에 전달된 식 트리 다양화

런타임 상태에 따라 다양한 식을 LINQ 메서드에 전달할 수 있습니다.

' 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)

LinqKitPredicateBuilder와 같은 타사 라이브러리를 사용하여 다양한 하위 식을 작성할 수도 있습니다.

' 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)

팩터리 메서드를 사용하여 식 트리 및 쿼리 생성

지금까지 모든 예제에서 컴파일 시간의 요소 형식 String 및 이에 따른 쿼리 형식 IQueryable(Of String)을(를) 알아보았습니다. 요소 형식의 쿼리에 구성 요소를 추가하거나 요소 형식에 따라 다른 구성 요소를 추가해야 할 수 있습니다. System.Linq.Expressions.Expression에서 팩터리 메서드를 사용하여 처음부터 식 트리를 만들어서 런타임에 식을 특정 요소 형식에 맞게 조정할 수 있습니다.

Expression(Of TDelegate) 생성

LINQ 메서드 중 하나에 전달할 식을 생성하면 실제로는 Expression(Of TDelegate)의 인스턴스가 생성됩니다. 여기서 TDelegate은(는) Func(Of String, Boolean), Action 또는 사용자 지정 대리자 형식과 같은 일부 대리자 형식입니다.

Expression(Of TDelegate)는 다음과 같은 완전한 람다 식을 나타내는 LambdaExpression에서 상속됩니다.

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

LambdaExpression에는 다음 두 가지 구성 요소가 있습니다.

  • 매개 변수 목록((x As String))은 Parameters 속성으로 표시됩니다.
  • 본문(x.StartsWith("a"))은 Body 속성으로 표시됩니다.

Expression(Of TDelegate)을 구성하는 기본 단계는 다음과 같습니다.

  • Parameter 팩터리 메서드를 사용하여 람다 식에서 각 매개 변수(있는 경우)에 대해 ParameterExpression 개체를 정의합니다.

    Dim x As ParameterExpression = Parameter(GetType(String), "x")
    
  • 사용자가 정의한 ParameterExpressionExpression의 팩터리 메서드를 사용하여 LambdaExpression의 본문을 구성합니다. 예를 들어 x.StartsWith("a")를 나타내는 식은 다음과 같이 생성할 수 있습니다.

    Dim body As Expression = [Call](
        x,
        GetType(String).GetMethod("StartsWith", {GetType(String)}),
        Constant("a")
    )
    
  • 적절한 Lambda 팩터리 메서드 오버로드를 사용하여 컴파일 시간 형식의 Expression(Of TDelegate)에 매개 변수와 본문을 래핑합니다.

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

다음 섹션에서는 LINQ 메서드에 전달하기 위해 Expression(Of TDelegate)을 구성하려는 시나리오를 설명하고 팩터리 메서드를 사용하여 이를 수행하는 방법에 대한 완전한 예를 제공합니다.

시나리오

여러 엔터티 형식이 있다고 가정해 보겠습니다.

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

해당 엔터티 형식에 대해 해당 string 필드 중 하나에 지정된 텍스트가 포함된 엔터티만 필터링하고 반환하려고 합니다. Person의 경우 FirstNameLastName 속성을 검색하려고 합니다.

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

그러나 Car의 경우 Model 속성만 검색하려고 합니다.

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

IQueryable(Of Person)IQueryable(Of Car)에 대해 하나씩 사용자 지정 함수를 작성할 수 있지만, 다음 함수는 특정 요소 형식과 관계없이 이 필터링을 기존 쿼리에 추가합니다.

예시

' 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 함수는 IQueryable만이 아니라 IQueryable(Of T)을 사용하고 반환하기 때문에 텍스트 필터 뒤에 컴파일 시간 형식의 쿼리 요소를 더 추가할 수 있습니다.

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)

IQueryable의 식 트리에 메서드 호출 노드 추가

IQueryable(Of T) 대신 IQueryable이(가) 있는 경우 제네릭 LINQ 메서드를 직접 호출할 수 없습니다. 한 가지 대안은 위와 같이 내부 식 트리를 빌드하고 리플렉션을 사용하여 식 트리를 전달하는 동안 적절한 LINQ 메서드를 호출하는 것입니다.

LINQ 메서드 호출을 나타내는 MethodCallExpression으로 트리 전체를 래핑하여 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

이 경우 컴파일 시간 T 제네릭 자리 표시자가 없으므로 컴파일 시간 형식 정보가 필요하지 않고 Expression(Of TDelegate) 대신 LambdaExpression을(를) 생성하는 Lambda 오버로드를 사용하게 됩니다.

동적 LINQ 라이브러리

팩터리 메서드를 사용하여 식 트리를 생성하는 작업은 비교적 복잡하며, 문자열을 작성하는 작업이 더 쉽습니다. 동적 LINQ 라이브러리Queryable에서 표준 LINQ 메서드에 해당하며 식 트리 대신 특수 구문에서 문자열을 허용하는 IQueryable에 확장 메서드 세트를 공개합니다. 라이브러리는 문자열에서 적절한 식 트리를 생성하며 결과로 변환된 IQueryable을 반환할 수 있습니다.

예를 들어 이전 예제(식 트리 생성 포함)는 다음과 같이 다시 작성할 수 있습니다.

' 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

참고 항목