Query basata sullo stato di runtime
Nella maggior parte delle query LINQ la forma generale della query viene impostata nel codice. È possibile filtrare gli elementi usando una clausola where
, ordinare la raccolta di output usando orderby
, raggruppare gli elementi o eseguire alcuni calcoli. Il codice potrebbe fornire parametri per il filtro, la chiave di ordinamento o altre espressioni che fanno parte della query. Tuttavia, la forma complessiva della query non può cambiare. In questo articolo vengono illustrate le tecniche per usare l'interfaccia System.Linq.IQueryable<T> e i tipi che la implementano per modificare la forma di una query in fase di esecuzione.
Queste tecniche vengono usate per creare query in fase di esecuzione, in cui determinati input dell'utente o stati di runtime modificano i metodi di query da usare come parte della query. Si vuole modificare la query aggiungendo, rimuovendo o modificando clausole della query.
Nota
Assicurati di aggiungere using System.Linq.Expressions;
e using static System.Linq.Expressions.Expression;
nella parte superiore del file .cs.
Si consideri il codice che definisce un'interfaccia IQueryable o un'interfaccia IQueryable<T> per un'origine dati:
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);
Ogni volta che si esegue il codice precedente, viene eseguita la stessa query esatta. Si apprenderà come modificare la query, estendendola o modificandola. Fondamentalmente, un oggetto IQueryable ha due componenti:
- Expression - una rappresentazione indipendente dal linguaggio e dall'origine dati dei componenti della query corrente, sotto forma di albero delle espressioni.
- Provider— un'istanza di un provider LINQ, che sa come materializzare la query corrente in un valore o un set di valori.
Nel contesto dell'esecuzione dinamica di query, il provider rimane in genere lo stesso, mentre l’albero delle espressioni della query è diverso per ogni query.
Gli alberi delle espressioni non sono modificabili. Se si vuole un albero delle espressioni diverso, e quindi una query diversa, è necessario convertire l'albero delle espressioni esistente in un nuovo albero delle espressioni. Le sezioni seguenti descrivono tecniche specifiche per l'esecuzione di query in modo diverso in risposta allo stato di runtime:
- Usare lo stato di runtime dall'interno dell'albero delle espressioni
- Chiamare altri metodi LINQ
- Variare l'albero delle espressioni passato nei metodi LINQ
- Costruire un albero delle espressioni Expression<TDelegate> usando i metodi factory in Expression
- Aggiungere nodi di chiamata al metodo a un albero delle espressioni di IQueryable
- Costruire stringhe e usare la libreria LINQ dinamica
Ognuna delle tecniche abilita più funzionalità, ma a costo di una maggiore complessità.
Usare lo stato di runtime dall'interno dell'albero delle espressioni
Il modo più semplice per eseguire query in modo dinamico consiste nel fare riferimento allo stato di runtime direttamente nella query tramite una variabile chiusa, ad esempio length
nell'esempio di codice seguente:
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
L'albero delle espressioni interne, e quindi la query, non viene modificato. La query restituisce valori diversi solo perché il valore di length
è stato modificato.
Chiamare altri metodi LINQ
In genere, i metodi LINQ predefiniti in Queryable vengono eseguiti in due passaggi:
- Eseguire il wrapping dell'albero delle espressioni corrente in un oggetto MethodCallExpression che rappresenta la chiamata al metodo.
- Passare di nuovo l'albero delle espressioni di cui è stato eseguito il wrapping al provider, per restituire un valore tramite il metodo IQueryProvider.Execute del provider, oppure per restituire un oggetto query convertito tramite il metodo IQueryProvider.CreateQuery.
È possibile sostituire la query originale con il risultato di un metodo che restituisce System.Linq.IQueryable<T>, per ottenere una nuova query. È possibile usare lo stato di runtime, come nell'esempio seguente:
// bool sortByLength = /* ... */;
var qry = companyNamesSource;
if (sortByLength)
{
qry = qry.OrderBy(x => x.Length);
}
Variare l'albero delle espressioni passato nei metodi LINQ
È possibile passare espressioni diverse ai metodi LINQ, a seconda dello stato di runtime:
// 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);
È anche possibile comporre le varie sottoespressioni usando un'altra libreria, come PredicateBuilder di 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);
Costruire alberi delle espressioni e query usando i metodi factory
In tutti gli esempi fino a questo punto erano noti il tipo di elemento in fase di compilazione, string
, e quindi il tipo della query, IQueryable<string>
. Potrebbe essere necessario aggiungere componenti a una query di qualsiasi tipo di elemento o aggiungere componenti diversi, a seconda del tipo di elemento. È possibile creare alberi delle espressioni da zero, usando i metodi factory in System.Linq.Expressions.Expression, e quindi adattare l'espressione in fase di esecuzione a un tipo di elemento specifico.
Creazione di una classe Expression<TDelegate>
Quando si costruisce un'espressione da passare a uno dei metodi LINQ, si costruisce effettivamente un'istanza di System.Linq.Expressions.Expression<TDelegate>, dove TDelegate
è un tipo delegato, ad esempio Func<string, bool>
, Action
o un tipo delegato personalizzato.
System.Linq.Expressions.Expression<TDelegate> eredita da LambdaExpression, che rappresenta un'espressione lambda completa come l'esempio seguente:
Expression<Func<string, bool>> expr = x => x.StartsWith("a");
Un oggetto LambdaExpression ha due componenti:
- Un elenco di parametri,
(string x)
, rappresentato dalla proprietà Parameters. - Un corpo,
x.StartsWith("a")
, rappresentato dalla proprietà Body.
I passaggi di base per la creazione di un'istanza di Expression<TDelegate> sono i seguenti:
- Definire gli oggetti ParameterExpression per ognuno dei parametri (se presenti) nell'espressione lambda, usando il metodo factory Parameter.
ParameterExpression x = Parameter(typeof(string), "x");
- Costruire il corpo di LambdaExpression, usando l'espressione ParameterExpression definita e i metodi factory in Expression. Ad esempio, un'espressione che rappresenta
x.StartsWith("a")
può essere costruita come segue:Expression body = Call( x, typeof(string).GetMethod("StartsWith", [typeof(string)])!, Constant("a") );
- Eseguire il wrapping dei parametri e del corpo in un'espressione<>TDelegate, usando l'overload appropriato del metodo factory Lambda:
Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
Le sezioni seguenti descrivono uno scenario in cui è possibile creare un Expression<TDelegate> da passare a un metodo LINQ. Fornisce un esempio completo di come eseguire questa operazione usando i metodi factory.
Costruire una query completa in fase di esecuzione
Si vogliono scrivere query che funzionano con più tipi di entità:
record Person(string LastName, string FirstName, DateTime DateOfBirth);
record Car(string Model, int Year);
Per uno di questi tipi di entità, è necessario filtrare e restituire solo le entità con un testo specificato all'interno di uno dei relativi campi string
. Per Person
, è consigliabile eseguire ricerche nelle proprietà FirstName
e LastName
:
string term = /* ... */;
var personsQry = new List<Person>()
.AsQueryable()
.Where(x => x.FirstName.Contains(term) || x.LastName.Contains(term));
Ma per Car
, si vuole cercare solo la proprietà Model
:
string term = /* ... */;
var carsQry = new List<Car>()
.AsQueryable()
.Where(x => x.Model.Contains(term));
Anche se è possibile scrivere una funzione personalizzata per IQueryable<Person>
e un'altra per IQueryable<Car>
, la funzione seguente aggiunge questo filtro a qualsiasi query esistente, indipendentemente dal tipo di elemento specifico.
// 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);
}
Dato che la funzione TextFilter
accetta e restituisce un IQueryable<T> (e non solo un IQueryable), è possibile aggiungere altri elementi di query tipizzati in fase di compilazione dopo il filtro di testo.
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);
Aggiungere nodi di chiamata al metodo all'albero delle espressioni di IQueryable<TDelegate>
Se si dispone di un'interfaccia IQueryable invece di IQueryable<T>, non è possibile chiamare direttamente i metodi LINQ generici. Un'alternativa consiste nel creare l'albero delle espressioni interne come illustrato nell'esempio precedente e usare la reflection per richiamare il metodo LINQ appropriato durante il passaggio dell'albero delle espressioni.
È anche possibile duplicare la funzionalità del metodo LINQ eseguendo il wrapping dell'intero albero in un oggetto MethodCallExpression che rappresenta una chiamata al metodo 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);
}
In questo caso, non è disponibile un segnaposto generico T
in fase di compilazione, quindi si userà l'overload Lambda che non richiede informazioni sul tipo in fase di compilazione e che genera LambdaExpression anziché Expression<TDelegate>.
Libreria LINQ dinamica
La costruzione di alberi delle espressioni usando i metodi factory è relativamente complessa. È più facile comporre stringhe. La libreria LINQ dinamica espone un set di metodi di estensione su IQueryable corrispondenti ai metodi LINQ standard in Queryable e che accettano stringhe in una sintassi speciale anziché alberi delle espressioni. La libreria genera l'albero delle espressioni appropriato dalla stringa e può restituire il risultato tradotto IQueryable.
Ad esempio, l'esempio precedente può essere riscritto come segue:
// 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);
}