Interrogation basée sur l'état d'exécution (Visual Basic)
Considérons un code qui définit un IQueryable ou un IQueryable(Of T) par rapport à une source de données :
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)
Chaque fois que vous exécutez ce code, exactement la même requête est exécutée. Ce n’est généralement pas très utile, car vous voudrez peut-être que votre code exécute différentes requêtes en fonction des conditions au moment de l’exécution. Cet article explique comment exécuter une autre requête en fonction de l’état du runtime.
IQueryable / IQueryable(Of T) et arbres d'expression
Un IQueryable a deux composants essentiels :
- Expression — représentation, indépendante du langage et des sources de données, des composants de la requête actuelle, sous la forme d’une arborescence d’expression.
- Provider — instance d’un fournisseur LINQ, qui sait comment matérialiser la requête actuelle en une valeur ou un ensemble de valeurs.
Dans le contexte de requêtes dynamiques, le fournisseur reste généralement le même tandis que l’arborescence d’expression de la requête diffère d’une requête à l’autre.
Les arborescences d’expression sont immuables. Si vous souhaitez une autre arborescence d’expression, et donc une autre requête, vous devez traduire l’arborescence d’expression existante en une nouvelle, et donc en une nouvelle IQueryable.
Les sections suivantes décrivent des techniques spécifiques de requête différentes en réponse à l’état du runtime :
- Utiliser l’état du runtime à partir de l’arborescence d’expression
- Appeler des méthodes LINQ supplémentaires
- Changer d’arborescence d’expression passée dans les méthodes LINQ
- Construire un arbre d'expression Expression(Of TDelegate) en utilisant les méthodes d'usine à Expression
- Ajouter des nœuds d’appel de méthode à l’arborescence d’expression d’une IQueryable
- Construire des chaînes et utiliser la bibliothèque Dynamic LINQ
Utiliser l’état du runtime à partir de l’arborescence d’expression
En supposant que le fournisseur LINQ le prend en charge, le moyen le plus simple d’interroger dynamiquement consiste à référencer l’état du runtime directement dans la requête via une variable « closed-over », comme length
dans l’exemple de code suivant :
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
L’arborescence d’expression interne, et donc la requête, n’ont pas été modifiées. La requête retourne des valeurs différentes uniquement parce que la valeur de length
a été modifiée.
Appeler des méthodes LINQ supplémentaires
En règle générale, les méthodes LINQ intégrées sur Queryable effectuent deux étapes :
- Wrapper l’arborescence d’expression actuelle dans une MethodCallExpression représentant l’appel de méthode.
- Passer l’arborescence d’expression wrappée au fournisseur, soit pour retourner une valeur via la méthode IQueryProvider.Execute du fournisseur, soit pour retourner un objet de requête traduit via la méthode IQueryProvider.CreateQuery.
Vous pouvez remplacer la requête originale par le résultat d'une méthode IQueryable(Of T) pour obtenir une nouvelle requête. Vous pouvez faire cela sous conditions en fonction de l’état du runtime, comme dans l’exemple suivant :
' Dim sortByLength As Boolean = ...
Dim qry = companyNamesSource
If sortByLength Then qry = qry.OrderBy(Function(x) x.Length)
Changer d’arborescence d’expression passée dans les méthodes LINQ
Vous pouvez passer différentes expressions aux méthodes LINQ, en fonction de l’état du 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)
Vous voudrez probablement aussi composer les différentes sous-expressions à l’aide d’une bibliothèque tierce telle que celle de 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)
Construire des arborescences d’expression et des requêtes avec des méthodes de fabrique
Dans tous les exemples vus jusqu’ici, nous connaissions le type d’élément au moment de la compilation, String
, et donc le type de la requête, IQueryable(Of String)
. Vous devrez peut-être ajouter des composants à une requête de n’importe quel type d’élément ou ajouter des composants différents, en fonction du type d’élément. Vous pouvez créer des arborescences d’expression de la base jusqu’en haut en utilisant les méthodes de fabrique sur System.Linq.Expressions.Expression, et ainsi adapter l’expression au moment de l’exécution à un type d’élément spécifique.
Construction d'une expression (Of TDelegate)
Lorsque vous construisez une expression à transmettre à l'une des méthodes LINQ, vous construisez en fait une instance d'Expression(Of TDelegate), où TDelegate
se trouve un type de délégué tel que Func(Of String, Boolean)
, Action
, ou un type de délégué personnalisé.
Expression(Of TDelegate) hérite de LambdaExpression, qui représente une expression lambda complète comme la suivante :
Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")
Une LambdaExpression a deux composants :
- Une liste de paramètres —
(x As String)
— représentée par la propriété Parameters. - Un corps —
x.StartsWith("a")
— représenté par la propriété Body.
Les étapes de base de la construction d'une Expression(Of TDelegate) sont les suivantes :
Définir des objets ParameterExpression pour chacun des paramètres (le cas échéant) dans l’expression lambda, en utilisant la méthode de fabrique Parameter.
Dim x As ParameterExpression = Parameter(GetType(String), "x")
Construire le corps de votre LambdaExpression en utilisant la ou les ParameterExpressions que vous avez définies et les méthodes de fabrique sur Expression. Par exemple, une expression représentant
x.StartsWith("a")
peut être construite comme ceci :Dim body As Expression = [Call]( x, GetType(String).GetMethod("StartsWith", {GetType(String)}), Constant("a") )
Envelopper les paramètres et le corps dans une Expression(Of TDelegate) typée par le temps de compilation, en utilisant la surcharge de la Lambda méthode d'usine appropriée :
Dim expr As Expression(Of Func(Of String, Boolean)) = Lambda(Of Func(Of String, Boolean))(body, x)
Les sections suivantes décrivent un scénario dans lequel vous pourriez vouloir construire une Expression(Of TDelegate) à passer dans une méthode LINQ, et fournissent un exemple complet de la façon de le faire en utilisant les méthodes d'usine.
Scénario
Supposons que vous avez plusieurs types d’entités :
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
Pour l’un de ces types d’entités, vous souhaitez filtrer et retourner uniquement les entités qui ont un texte donné dans l’un de leurs champs string
. Pour Person
, vous souhaitez rechercher dans les propriétés FirstName
et LastName
:
' Dim term = ...
Dim personsQry = (New List(Of Person)).AsQueryable.
Where(Function(x) x.FirstName.Contains(term) OrElse x.LastName.Contains(term))
Par contre, pour Car
, vous souhaitez rechercher uniquement dans la propriété Model
:
' Dim term = ...
Dim carsQry = (New List(Of Car)).AsQueryable.
Where(Function(x) x.Model.Contains(term))
Bien que vous puissiez écrire une fonction personnalisée pour IQueryable(Of Person)
et une autre pour IQueryable(Of Car)
, la fonction suivante ajoute ce filtrage à n’importe quelle requête existante, quel que soit le type d’élément spécifique.
Exemple
' 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
Étant donné que la TextFilter
fonction prend et renvoie un IQueryable(Of T) (et pas seulement un IQueryable ), vous pouvez ajouter d'autres éléments de requête typés par le temps de compilation après le filtre de texte.
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)
Ajouter des nœuds d’appel de méthode à l’arborescence d’expression d’une IQueryable
Si vous avez un IQueryable au lieu d'un IQueryable(Of T), vous ne pouvez pas appeler directement les méthodes génériques LINQ. Une alternative consiste à créer l’arborescence d’expression interne comme ci-dessus et à utiliser la réflexion pour appeler la méthode LINQ appropriée lors du passage de l’arborescence d’expression.
Vous pouvez également dupliquer les fonctionnalités de la méthode LINQ, en wrappant l’arborescence entière dans une MethodCallExpression qui représente un appel à la méthode 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
Dans ce cas, vous n'avez pas de place T
générique à la compilation, vous utiliserez donc Lambda la surcharge qui ne nécessite pas d'informations de type à la compilation et qui produit un LambdaExpression au lieu d'une Expression(Of TDelegate).
La bibliothèque Dynamic LINQ
La construction d’arborescences d’expression à l’aide de méthodes de fabrique est relativement complexe, il est plus facile de composer des chaînes. La bibliothèque Dynamic LINQ expose un ensemble de méthodes d’extension sur IQueryable correspondant aux méthodes LINQ standard sur Queryable, et qui acceptent les chaînes dans une syntaxe spéciale au lieu d’arborescences d’expression. La bibliothèque génère l’arborescence d’expression appropriée à partir de la chaîne et peut retourner la IQueryable traduite qui en résulte.
Par exemple, l'exemple précédent (y compris la construction de l'arbre d'expression) pourrait être réécrit comme suit :
' 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