Scrivere query LINQ in C# per eseguire query sui dati

La maggior parte delle query presenti nella documentazione di Language Integrated Query (LINQ) sono scritte usando la sintassi di query dichiarativa di LINQ. Tuttavia, la sintassi di query deve essere convertita in chiamate al metodo per Common Language Runtime (CLR) di .NET quando il codice viene compilato. Queste chiamate al metodo richiamano gli operatori query standard, che hanno nomi come Where, Select, GroupBy, Join, Max e Average. È possibile chiamarli direttamente usando la sintassi di metodo anziché la sintassi di query.

La sintassi di query e la sintassi di metodo sono semanticamente identiche, ma la sintassi di query è spesso più semplice e più facile da leggere. Alcune query devono essere espresse come chiamate al metodo. Ad esempio, è necessario usare una chiamata al metodo per esprimere una query che recupera il numero di elementi che soddisfano una determinata condizione. È necessario usare una chiamata al metodo anche per una query che recupera l'elemento con il valore massimo in una sequenza di origine. Nella documentazione di riferimento per gli operatori query standard nello spazio dei nomi System.Linq viene usata in genere la sintassi di metodo. È necessario acquisire familiarità con l'uso della sintassi di metodo nelle query e nelle espressioni di query stesse.

Metodi di estensione degli operatori query standard

Nell'esempio seguente viene illustrata un'espressione di query semplice e la query semanticamente equivalente scritta come query basata su metodo.

int[] numbers = [ 5, 10, 8, 3, 6, 12 ];

//Query syntax:
IEnumerable<int> numQuery1 =
    from num in numbers
    where num % 2 == 0
    orderby num
    select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

foreach (int i in numQuery1)
{
    Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
    Console.Write(i + " ");
}

L'output dei due esempi è identico. Si noterà che il tipo della variabile di query è lo stesso in entrambi i formati: IEnumerable<T>.

Per capire meglio la query basata su metodo, esaminiamola più da vicino. Sul lato destro dell'espressione, si può notare che la clausola where viene ora espressa come metodo di istanza per l'oggetto numbers che ha un tipo di IEnumerable<int>. Chi ha familiarità con l'interfaccia generica IEnumerable<T> sa che non ha un metodo Where. Tuttavia, se si richiama l'elenco di completamento IntelliSense nell'IDE di Visual Studio, si vedrà non solo un metodo Where ma molti altri metodi, ad esempio Select, SelectMany, Join e Orderby. Questi metodi implementano gli operatori query standard.

Screenshot di tutti gli operatori query standard in Intellisense.

Anche se sembra che IEnumerable<T> includa altri metodi, non è così. Gli operatori query standard vengono implementati come metodi di estensione. I metodi di estensione "estendono" un tipo esistente. Possono essere chiamati come se fossero metodi di istanza per il tipo. Gli operatori di query standard estendono IEnumerable<T> e questo è il motivo per cui è possibile scrivere numbers.Where(...).

Per usare i metodi di estensione, è possibile inserirli nell'ambito con le direttive using. Dal punto di vista dell'applicazione, un metodo di estensione e un metodo di istanza normale sono la stessa cosa.

Per altre informazioni sui metodi di estensione, vedere Metodi di estensione. Per altre informazioni sugli operatori di query standard, vedere Panoramica degli operatori di query standard (C#). Alcuni provider LINQ, ad esempio Entity Framework e LINQ to XML, implementano i propri operatori query standard e metodi di estensione per altri tipi oltre a IEnumerable<T>.

Espressioni lambda

Nell'esempio precedente, si può notare che l'espressione condizionale (num % 2 == 0) viene passata come argomento inline al metodo Enumerable.Where: Where(num => num % 2 == 0). Questa espressione inline è un'espressione lambda. È un modo pratico per scrivere codice che altrimenti dovrebbe essere scritto in un formato più complesso. L'elemento num a sinistra dell'operatore è la variabile di input che corrisponde a num nell'espressione di query. Il compilatore è in grado di dedurre il tipo di num poiché sa che numbers è un tipo IEnumerable<T> generico. Il corpo dell'espressione lambda è identico all'espressione nella sintassi di query o in qualsiasi altra espressione o istruzione di C#. Può includere chiamate al metodo e altra logica complessa. Il valore restituito è semplicemente il risultato dell'espressione. Alcune query possono essere espresse solo nella sintassi di metodo e alcune di esse richiedono le espressioni lambda. Le espressioni lambda sono uno strumento potente e flessibile della casella degli strumenti di LINQ.

Componibilità delle query

Nell'esempio di codice precedente il metodo Enumerable.OrderBy viene richiamato usando l'operatore punto nella chiamata a Where. Where produce una sequenza filtrata e quindi Orderby ordina la sequenza prodotta da Where. Poiché le query restituiscono un oggetto IEnumerable, è necessario comporle nella sintassi di metodo concatenando le chiamate al metodo. Il compilatore esegue questa composizione quando si scrivono query usando la sintassi di query. Poiché una variabile di query non archivia i risultati della query, è possibile modificarla o usarla come base per una nuova query in qualsiasi momento, anche dopo averla eseguita.

Gli esempi seguenti illustrano alcune semplici query LINQ usando ogni approccio indicato sopra.

Nota

Queste query operano su raccolte in memoria semplici, tuttavia, la sintassi di base è identica a quella usata in LINQ to Entities e LINQ to XML.

Esempio - Sintassi di query

La maggior parte delle query viene scritta con la sintassi di query per creare espressioni di query. Nell'esempio seguente sono riportate tre espressioni di query. La prima espressione di query dimostra in che modo si filtrano o si limitano i risultati applicando le condizioni con una clausola where. Restituisce tutti gli elementi nella sequenza di origine i cui valori sono maggiori di 7 o minori di 3. La seconda espressione illustra come ordinare i risultati restituiti. La terza espressione illustra come raggruppare i risultati in base a una chiave. Questa query restituisce due gruppi in base alla prima lettera della parola.

List<int> numbers = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

// The query variables can also be implicitly typed by using var

// Query #1.
IEnumerable<int> filteringQuery =
    from num in numbers
    where num is < 3 or > 7
    select num;

// Query #2.
IEnumerable<int> orderingQuery =
    from num in numbers
    where num is < 3 or > 7
    orderby num ascending
    select num;

// Query #3.
string[] groupingQuery = ["carrots", "cabbage", "broccoli", "beans", "barley"];
IEnumerable<IGrouping<char, string>> queryFoodGroups =
    from item in groupingQuery
    group item by item[0];

Il tipo delle query è IEnumerable<T>. Tutte queste query potrebbero essere scritte usando var come indicato nell'esempio seguente:

var query = from num in numbers...

In ognuno degli esempi precedenti le query non vengono effettivamente eseguite finché non si esegue l'iterazione della variabile di query in un'istruzione foreach o in un'altra istruzione.

Esempio - Sintassi del metodo

Alcune operazioni di query devono essere espresse come una chiamata al metodo. I metodi più comuni sono quelli che restituiscono valori numerici singleton, ad esempio Sum, Max, Min, Average e così via. Questi metodi devono sempre essere chiamati per ultimi in una query poiché restituiscono un singolo valore e non possono essere usati come origine per un'operazione di query aggiuntiva. Nell'esempio seguente viene illustrata una chiamata al metodo in un'espressione di query:

List<int> numbers1 = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
List<int> numbers2 = [15, 14, 11, 13, 19, 18, 16, 17, 12, 10];

// Query #4.
double average = numbers1.Average();

// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

Se il metodo usa i parametri System.Action o System.Func<TResult>, questi argomenti vengono specificati sotto forma di espressione lambda, come illustrato nell'esempio seguente:

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

Nelle query precedenti, solo la Query #4 viene eseguita immediatamente, perché restituisce un singolo valore e non una raccolta generica di IEnumerable<T>. Il metodo stesso usa foreach o codice simile per calcolarne il valore.

Ognuna delle query precedenti può essere scritta usando la tipizzazione implicita con `var``, come illustrato nell'esempio seguente:

// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

Esempio - Sintassi di query e di metodo mista

In questo esempio viene illustrato come usare la sintassi di metodo per i risultati di una clausola di query. È sufficiente racchiudere l'espressione di query tra parentesi e quindi applicare l'operatore punto e chiamare il metodo. Nell'esempio seguente la query n. 7 restituisce un conteggio dei numeri il cui valore è compreso tra 3 e 7. In generale, tuttavia, è preferibile usare una seconda variabile per archiviare il risultato della chiamata al metodo. In questo modo è meno probabile che si crei confusione con i risultati della query.

// Query #7.

// Using a query expression with method syntax
var numCount1 = (
    from num in numbers1
    where num is > 3 and < 7
    select num
).Count();

// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
    from num in numbers1
    where num is > 3 and < 7
    select num;

var numCount2 = numbersQuery.Count();

Poiché la query n. 7 restituisce un singolo valore e non una raccolta, la query viene eseguita immediatamente.

La query precedente può essere scritta usando la tipizzazione implicita con var, come segue:

var numCount = (from num in numbers...

Può essere scritta nella sintassi di metodo come indicato di seguito:

var numCount = numbers.Count(n => n is > 3 and < 7);

Può essere scritta usando la tipizzazione esplicita, come indicato di seguito:

int numCount = numbers.Count(n => n is > 3 and < 7);

Specificare dinamicamente i filtri dei predicati in fase di esecuzione

In alcuni casi, fino alla fase di esecuzione non si sa quanti predicati è necessario applicare agli elementi di origine nella clausola where. Un modo per specificare dinamicamente più filtri di predicato consiste nell'usare il metodo Contains, come illustrato nell'esempio seguente. La query restituisce risultati diversi in base al valore di id al momento dell'esecuzione della query.

int[] ids = [111, 114, 112];

var queryNames =
    from student in students
    where ids.Contains(student.ID)
    select new
    {
        student.LastName,
        student.ID
    };

foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Garcia: 114
    O'Donnell: 112
    Omelchenko: 111
 */

// Change the ids.
ids = [122, 117, 120, 115];

// The query will now return different results
foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Adams: 120
    Feng: 117
    Garcia: 115
    Tucker: 122
 */

È possibile usare istruzioni del flusso di controllo, ad esempio if... else o switch, per selezionare tra query alternative predeterminate. Nell'esempio seguente, studentQuery usa una clausola where diversa se il valore di runtime di oddYear è true o false.

void FilterByYearType(bool oddYear)
{
    IEnumerable<Student> studentQuery = oddYear
        ? (from student in students
           where student.Year is GradeLevel.FirstYear or GradeLevel.ThirdYear
           select student)
        : (from student in students
           where student.Year is GradeLevel.SecondYear or GradeLevel.FourthYear
           select student);
    var descr = oddYear ? "odd" : "even";
    Console.WriteLine($"The following students are at an {descr} year level:");
    foreach (Student name in studentQuery)
    {
        Console.WriteLine($"{name.LastName}: {name.ID}");
    }
}

FilterByYearType(true);

/* Output:
    The following students are at an odd year level:
    Fakhouri: 116
    Feng: 117
    Garcia: 115
    Mortensen: 113
    Tucker: 119
    Tucker: 122
 */

FilterByYearType(false);

/* Output:
    The following students are at an even year level:
    Adams: 120
    Garcia: 114
    Garcia: 118
    O'Donnell: 112
    Omelchenko: 111
    Zabokritski: 121
 */

Gestire i valori Null nelle espressioni di query

In questo esempio viene illustrato come gestire i possibili valori Null nelle raccolte di origine. Una raccolta di oggetti, ad esempio IEnumerable<T>, può contenere elementi il cui valore è Null. Se una raccolta di origine è null o contiene un elemento il cui valore è null e la query non gestisce valori null, quando si esegue la query viene generata un'eccezione NullReferenceException.

È possibile codificare in modo sicuro per evitare un'eccezione di riferimento Null come illustrato nell'esempio seguente:

var query1 =
    from c in categories
    where c != null
    join p in products on c.ID equals p?.CategoryID
    select new
    {
        Category = c.Name,
        Name = p.Name
    };

Nell'esempio precedente la clausola where esclude tutti gli elementi Null nella sequenza di categorie. Questa tecnica è indipendente dal controllo Null nella clausola join. In questo esempio è possibile usare l'espressione condizionale con Null poiché Products.CategoryID è di tipo int?, vale a dire una sintassi abbreviata di Nullable<int>.

Se in una clausola join solo una delle chiavi di confronto è un tipo di valore nullable, è possibile eseguire il cast delle altre chiavi a un tipo di valore nullable nell'espressione di query. Nell'esempio seguente si supponga che EmployeeID sia una colonna contenente valori di tipo int?:

var query =
    from o in db.Orders
    join e in db.Employees
        on o.EmployeeID equals (int?)e.EmployeeID
    select new { o.OrderID, e.FirstName };

In ognuno degli esempi viene usata la parola chiave di query equals. È anche possibile usare i criteri di ricerca, che includono i criteri per is null e is not null. Questi modelli non sono consigliati nelle query LINQ perché i provider di query potrebbero non interpretare correttamente la nuova sintassi C#. Un provider di query è una libreria che converte le espressioni di query C# in un formato di dati nativo, ad esempio Entity Framework Core. I provider di query implementano l'interfaccia System.Linq.IQueryProvider per creare origini dati che implementano l'interfaccia System.Linq.IQueryable<T>.

Gestire le eccezioni nelle espressioni di query

Nel contesto di un'espressione di query è possibile chiamare qualsiasi metodo. Non chiamare in un'espressione di query i metodi che possono creare un effetto collaterale, ad esempio la modifica del contenuto dell'origine dati o la generazione di un'eccezione. Questo esempio illustra come evitare di generare eccezioni quando si chiamano i metodi in un'espressione di query senza violare le linee guida generali di .NET sulla gestione delle eccezioni. Tali linee guida stabiliscono che è accettabile intercettare un'eccezione specifica quando è evidente il motivo per cui viene generata in un contesto specificato. Per altre informazioni, vedere Suggerimenti per le eccezioni.

Nell'esempio finale viene illustrato come gestire quei casi in cui è necessario generare un'eccezione durante l'esecuzione di una query.

Nell'esempio seguente viene illustrato come spostare codice di gestione dell'eccezione al di fuori di un'espressione di query. Questo refactoring è possibile solo quando il metodo non dipende da variabili locali per la query. È più semplice gestire le eccezioni all'esterno dell'espressione di query.

// A data source that is very likely to throw an exception!
IEnumerable<int> GetData() => throw new InvalidOperationException();

// DO THIS with a datasource that might
// throw an exception.
IEnumerable<int>? dataSource = null;
try
{
    dataSource = GetData();
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation");
}

if (dataSource is not null)
{
    // If we get here, it is safe to proceed.
    var query =
        from i in dataSource
        select i * i;

    foreach (var i in query)
    {
        Console.WriteLine(i.ToString());
    }
}

Nel blocco catch (InvalidOperationException) dell'esempio precedente, gestire (o non gestire) l'eccezione nel modo appropriato per l'applicazione.

In alcuni casi la migliore risposta a un'eccezione generata all'interno di una query potrebbe essere l'arresto immediato dell'esecuzione della query. Nell'esempio seguente viene illustrato come gestire le eccezioni che potrebbero essere generate all'interno del corpo di una query. Si supponga che SomeMethodThatMightThrow possa potenzialmente generare un'eccezione che richiede l'arresto dell'esecuzione della query.

Il blocco try racchiude il ciclo foreach e non la query stessa. Il ciclo foreach è il punto in corrispondenza del quale viene eseguita la query. Le eccezioni di runtime vengono generate al momento dell'esecuzione della query. Pertanto, devono essere gestiti nel ciclo foreach.

// Not very useful as a general purpose method.
string SomeMethodThatMightThrow(string s) =>
    s[4] == 'C' ?
        throw new InvalidOperationException() :
        @"C:\newFolder\" + s;

// Data source.
string[] files = ["fileA.txt", "fileB.txt", "fileC.txt"];

// Demonstration query that throws.
var exceptionDemoQuery =
    from file in files
    let n = SomeMethodThatMightThrow(file)
    select n;

try
{
    foreach (var item in exceptionDemoQuery)
    {
        Console.WriteLine($"Processing {item}");
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}

/* Output:
    Processing C:\newFolder\fileA.txt
    Processing C:\newFolder\fileB.txt
    Operation is not valid due to the current state of the object.
 */

Ricordare di intercettare qualsiasi eccezione che si prevede di generare e/o eseguire eventuali operazioni di pulizia necessarie in un blocco finally.

Vedi anche