데이터 원본에 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 메서드 호출
- LINQ 메서드에 전달된 식 트리 변경
- 식(Of TDelegate) 식 트리를 Expression에서 팩터리 메서드를 사용하여 생성하십시오.
- 's 식 트리에 IQueryable메서드 호출 노드 추가
- 문자열을 생성하고 동적 LINQ 라이브러리 사용
식 트리 내에서 런타임 상태 사용
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 메서드 호출
일반적으로 기본 제공 LINQ 메서드 는 Queryable 다음 두 단계를 수행합니다.
- 현재 식 트리를 메서드 호출을 나타내는 MethodCallExpression에 래핑합니다.
- 래핑된 식 트리를 공급자에 다시 전달하여 공급자의 IQueryProvider.Execute 메서드를 통해 값을 반환하거나 메서드를 통해 IQueryProvider.CreateQuery 번역된 쿼리 개체를 반환합니다.
원래 쿼리를 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)
LinqKit의 PredicateBuilder와 같은 타사 라이브러리를 사용하여 다양한 하위 식도 작성할 수 있습니다.
' 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를 사용하여 처음부터 식 트리를 만들 수 있으므로 런타임에 식을 특정 요소 형식에 맞게 조정할 수 있습니다.
식 생성(TDelegate)
LINQ 메서드 중 하나에 전달할 식을 생성하는 경우, 실제로 특정 대리자 형식(, TDelegate
같은 대리자 형식 또는 사용자 지정 대리자 형식)의 Func(Of String, Boolean)
인스턴스를 생성하는 것입니다.
Expression(Of TDelegate)은 완전한 람다 식을 다음과 같이 나타내는 LambdaExpression을 상속합니다.
Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")
A LambdaExpression 에는 다음 두 가지 구성 요소가 있습니다.
-
(x As String)
로 표현되는 Parameters 속성의 매개 변수 목록입니다. -
x.StartsWith("a")
속성으로 표시되는 Body 본문입니다.
Expression(Of TDelegate)을 생성하는 기본 단계는 다음과 같습니다.
ParameterExpression 각 매개 변수(있는 경우)에 대한 객체를 Parameter 팩터리 메서드를 사용하여 람다 식에서 정의합니다.
Dim x As ParameterExpression = Parameter(GetType(String), "x")
정의한 LambdaExpression를 사용하고 ParameterExpression에 있는 팩토리 메소드를 활용하여 Expression의 본문을 구성하세요. 예를 들어 다음과 같이 나타내는
x.StartsWith("a")
식을 생성할 수 있습니다.Dim body As Expression = [Call]( x, GetType(String).GetMethod("StartsWith", {GetType(String)}), Constant("a") )
적절한 팩터리 메서드 오버로드를 사용하여 매개 변수와 본문을 컴파일 시간 형식의 식(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
에 대해 FirstName
및 LastName
속성을 검색하려고 합니다.
' 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(Of T)( 뿐만 IQueryable아니라)을 사용하고 반환하기 때문에 텍스트 필터 다음에 컴파일 시간 형식의 쿼리 요소를 더 추가할 수 있습니다.
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가 IQueryable(Of T) 대신 있는 경우, 제네릭 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
제네릭 자리 표시자가 없기 때문에 컴파일 시간 형식 정보가 필요하지 않는 Lambda 오버로드를 사용하게 되며, 이는 LambdaExpression 대신 를 생성합니다.
동적 LINQ 라이브러리
팩터리 메서드를 사용하여 식 트리를 생성하는 것은 비교적 복잡합니다. 문자열을 작성하는 것이 더 쉽습니다. 동적 LINQ 라이브러리는 표준 LINQ 메서드에 IQueryable 해당하는 확장 메서드 Queryable집합을 노출하고 식 트리 대신 특수 구문의 문자열을 허용합니다. 라이브러리는 문자열에서 적절한 식 트리를 생성하고 변환된 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
참고하십시오
.NET