Share via


LINQ: .NET Language-Integrated Query

 

Don Box, Anders Hejlsberg

Febbraio 2007

Si applica a:
   Nome di Visual Studio Code "Orcas"
   .NET Framework 3.5

Riepilogo: Le funzionalità di query per utilizzo generico aggiunte a .NET Framework si applicano a tutte le origini di informazioni, non solo ai dati relazionali o XML. Questa funzionalità è denominata .NET Language-Integrated Query (LINQ). (32 pagine stampate)

Contenuto

Query .NET Language-Integrated
Introduzione con operatori di query standard
Funzionalità del linguaggio che supportano il progetto LINQ
Altri operatori di query standard
Sintassi di query
LINQ to SQL: Integrazione SQL
LINQ to XML: Integrazione XML
Riepilogo

Query .NET Language-Integrated

Dopo due decenni, il settore ha raggiunto un punto stabile nell'evoluzione delle tecnologie di programmazione orientate agli oggetti (OO). I programmatori ora accettano funzionalità come classi, oggetti e metodi. Esaminando l'attuale e la prossima generazione di tecnologie, è emerso che la prossima grande sfida nella tecnologia di programmazione consiste nel ridurre la complessità dell'accesso e dell'integrazione di informazioni non definite in modo nativo tramite la tecnologia OO. Le due origini più comuni di informazioni non OO sono database relazionali e XML.

Invece di aggiungere funzionalità relazionali o specifiche di XML ai linguaggi di programmazione e al runtime, con il progetto LINQ è stato adottato un approccio più generale e si aggiungono funzionalità di query per utilizzo generico a .NET Framework che si applicano a tutte le origini di informazioni, non solo ai dati relazionali o XML. Questa funzionalità è denominata .NET Language-Integrated Query (LINQ).

Si usa il termine query integrata nel linguaggio per indicare che la query è una funzionalità integrata dei linguaggi di programmazione principali dello sviluppatore , ad esempio Visual C#, Visual Basic. La query integrata nel linguaggio consente alle espressioni di query di trarre vantaggio dai metadati avanzati, dal controllo della sintassi in fase di compilazione, dalla digitazione statica e da IntelliSense precedentemente disponibile solo per il codice imperativo. La query integrata nel linguaggio consente inoltre di applicare una singola funzionalità di query dichiarativa per utilizzo generico a tutte le informazioni in memoria, non solo alle informazioni provenienti da origini esterne.

.NET Language-Integrated Query definisce un set di operatori di query standard per utilizzo generico che consentono l'espressione delle operazioni di attraversamento, filtro e proiezione in modo diretto ma dichiarativo in qualsiasi . Linguaggio di programmazione basato su NET. Gli operatori di query standard consentono l'applicazione delle query a qualsiasi origine informazioni basata su T> IEnumerable<. LINQ consente a terze parti di aumentare il set di operatori di query standard con nuovi operatori specifici del dominio appropriati per il dominio o la tecnologia di destinazione. Inoltre, terze parti sono liberi di sostituire gli operatori di query standard con le proprie implementazioni che forniscono servizi aggiuntivi, ad esempio la valutazione remota, la traduzione di query, l'ottimizzazione e così via. Rispettando le convenzioni del modello LINQ, tali implementazioni godono dello stesso supporto di strumenti e integrazione del linguaggio degli operatori di query standard.

L'estendibilità dell'architettura di query viene usata nel progetto LINQ stesso per fornire implementazioni che funzionano su dati XML e SQL. Gli operatori di query su XML (LINQ to XML) usano una funzionalità XML efficiente e facile da usare per fornire funzionalità XPath/XQuery nel linguaggio di programmazione host. Gli operatori di query sui dati relazionali (LINQ to SQL) si basano sull'integrazione delle definizioni di schema basate su SQL nel sistema di tipi CLR (Common Language Runtime). Questa integrazione offre una forte digitazione sui dati relazionali mantenendo al tempo stesso la potenza espressiva del modello relazionale e le prestazioni della valutazione delle query direttamente nell'archivio sottostante.

Introduzione con operatori di query standard

Per visualizzare la query integrata nel linguaggio, si inizierà con un semplice programma C# 3.0 che usa gli operatori di query standard per elaborare il contenuto di una matrice:

using System;
using System.Linq;
using System.Collections.Generic;

class app {
  static void Main() {
    string[] names = { "Burke", "Connor", "Frank", 
                       "Everett", "Albert", "George", 
                       "Harris", "David" };

    IEnumerable<string> query = from s in names 
                               where s.Length == 5
                               orderby s
                               select s.ToUpper();

    foreach (string item in query)
      Console.WriteLine(item);
  }
}

Se si desidera compilare ed eseguire questo programma, verrà visualizzato come output:

BURKE
DAVID
FRANK
To understand how language-integrated query works, we need to dissect the
 first statement of our program.
IEnumerable<string> query = from s in names 
                           where s.Length == 5
                           orderby s
                           select s.ToUpper();

La query della variabile locale viene inizializzata con un'espressione di query. Un'espressione di query opera su una o più origini informazioni applicando uno o più operatori di query dagli operatori di query standard o dagli operatori specifici del dominio. Questa espressione usa tre degli operatori di query standard: Where, OrderBy e Select.

Visual Basic 9.0 supporta anche LINQ. Ecco l'istruzione precedente scritta in Visual Basic 9.0:

Dim query As IEnumerable(Of String) = From s in names _
                                     Where s.Length = 5 _
                   Order By s _
                   Select s.ToUpper()

Entrambe le istruzioni C# e Visual Basic illustrate qui usano espressioni di query. Analogamente all'istruzione foreach , le espressioni di query sono pratici abbreviate dichiarative sul codice che è possibile scrivere manualmente. Le istruzioni precedenti sono semanticamente identiche alla sintassi esplicita seguente illustrata in C#:

IEnumerable<string> query = names 
                            .Where(s => s.Length == 5) 
                            .OrderBy(s => s)
                            .Select(s => s.ToUpper());

Questa forma di query è denominata query basata su metodo . Gli argomenti degli operatori Where, OrderBy e Select sono denominati espressioni lambda, che sono frammenti di codice molto simili ai delegati. Consentono agli operatori di query standard di essere definiti singolarmente come metodi e raggruppati usando la notazione del punto. Insieme, questi metodi costituiscono la base per un linguaggio di query estendibile.

Funzionalità del linguaggio che supportano il progetto LINQ

LINQ è basato interamente sulle funzionalità del linguaggio per utilizzo generico, alcune delle quali sono nuove per C# 3.0 e Visual Basic 9.0. Ognuna di queste funzionalità ha utilità autonomamente, ma collettivamente queste funzionalità offrono un modo estendibile per definire query e API su cui è possibile eseguire query. In questa sezione vengono esaminate queste funzionalità del linguaggio e come contribuiscono a uno stile molto più diretto e dichiarativo delle query.

Espressioni lambda e alberi delle espressioni

Molti operatori di query consentono all'utente di fornire una funzione che esegue filtri, proiezioni o estrazione di chiavi. Le funzionalità di query si basano sul concetto di espressioni lambda, che offrono agli sviluppatori un modo pratico per scrivere funzioni che possono essere passate come argomenti per la valutazione successiva. Le espressioni lambda sono simili ai delegati CLR e devono rispettare una firma del metodo definita da un tipo delegato. Per illustrare questo problema, è possibile espandere l'istruzione precedente in un modulo equivalente ma più esplicito usando il tipo delegato Func :

Func<string, bool>   filter  = s => s.Length == 5;
Func<string, string> extract = s => s;
Func<string, string> project = s => s.ToUpper();

IEnumerable<string> query = names.Where(filter) 
                                 .OrderBy(extract)
                                 .Select(project);

Le espressioni lambda sono l'evoluzione naturale dei metodi anonimi in C# 2.0. Ad esempio, è possibile scrivere l'esempio precedente usando metodi anonimi come segue:

Func<string, bool>   filter  = delegate (string s) {
                                   return s.Length == 5; 
                               };

Func<string, string> extract = delegate (string s) { 
                                   return s; 
                               };

Func<string, string> project = delegate (string s) {
                                   return s.ToUpper(); 
                               };

IEnumerable<string> query = names.Where(filter) 
                                 .OrderBy(extract)
                                 .Select(project);

In generale, lo sviluppatore può usare metodi denominati, metodi anonimi o espressioni lambda con operatori di query. Le espressioni lambda hanno il vantaggio di fornire la sintassi più diretta e compatta per la creazione. In particolare, le espressioni lambda possono essere compilate come codice o dati, consentendo l'elaborazione delle espressioni lambda in fase di esecuzione da parte di optimizer, traduttori e analizzatori.

Lo spazio dei nomi System.Linq.Expressions definisce un tipo generico distinto, Expression<T>, che indica che un albero delle espressioni è desiderato per una determinata espressione lambda anziché per un corpo tradizionale del metodo basato su IL. Gli alberi delle espressioni sono rappresentazioni di dati in memoria efficienti delle espressioni lambda e rendono trasparente ed esplicita la struttura dell'espressione.

La determinazione del fatto che il compilatore genererà il codice eseguibile IL o un albero delle espressioni è determinato dal modo in cui viene usata l'espressione lambda. Quando un'espressione lambda viene assegnata a una variabile, a un campo o a un parametro il cui tipo è un delegato, il compilatore genera IL identico a quello di un metodo anonimo. Quando un'espressione lambda viene assegnata a una variabile, a un campo o a un parametro il cui tipo è Expression<T> per un tipo delegato T, il compilatore genera invece un albero delle espressioni.

Si considerino ad esempio le due dichiarazioni di variabili seguenti:

Func<int, bool>             f = n => n < 5;
Expression<Func<int, bool>> e = n => n < 5;

La variabile f è un riferimento a un delegato che è direttamente eseguibile:

bool isSmall = f(2); // isSmall is now true

La variabile e è un riferimento a un albero delle espressioni che non è direttamente eseguibile:

bool isSmall = e(2); // compile error, expressions == data

A differenza dei delegati, che sono effettivamente codice opaco, è possibile interagire con l'albero delle espressioni esattamente come qualsiasi altra struttura di dati nel programma.

Expression<Func<int, bool>> filter = n => n < 5;

BinaryExpression    body  = (BinaryExpression)filter.Body;
ParameterExpression left  = (ParameterExpression)body.Left;
ConstantExpression  right = (ConstantExpression)body.Right;

Console.WriteLine("{0} {1} {2}", 
                  left.Name, body.NodeType, right.Value);

Nell'esempio precedente viene scomposto l'albero delle espressioni in fase di esecuzione e viene stampata la stringa seguente:

n LessThan 5

Questa possibilità di trattare le espressioni come dati in fase di esecuzione è fondamentale per abilitare un ecosistema di librerie di terze parti che sfruttano le astrazioni di query di base che fanno parte della piattaforma. L'implementazione dell'accesso ai dati LINQ to SQL sfrutta questa funzionalità per convertire alberi delle espressioni in istruzioni T-SQL adatte per la valutazione nell'archivio.

Metodi di estensione

Le espressioni lambda sono una parte importante dell'architettura di query. I metodi di estensione sono un altro. I metodi di estensione combinano la flessibilità della "tipizzazione anatra" resa popolare nei linguaggi dinamici con le prestazioni e la convalida in fase di compilazione di linguaggi tipizzato in modo statico. Con i metodi di estensione, terze parti possono aumentare il contratto pubblico di un tipo con nuovi metodi, consentendo comunque ai singoli autori di tipi di fornire la propria implementazione specializzata di tali metodi.

I metodi di estensione sono definiti in classi statiche come metodi statici, ma sono contrassegnati con l'attributo [System.Runtime.CompilerServices.Extension] nei metadati CLR. I linguaggi sono invitati a fornire una sintassi diretta per i metodi di estensione. In C# i metodi di estensione sono indicati dal modificatore che deve essere applicato al primo parametro del metodo di estensione. Si esamini ora la definizione dell'operatore di query più semplice, Dove:

namespace System.Linq {
  using System;
  using System.Collections.Generic;

  public static class Enumerable {
    public static IEnumerable<T> Where<T>(
             this IEnumerable<T> source,
             Func<T, bool> predicate) {

      foreach (T item in source)
        if (predicate(item))
          yield return item;
    }
  }
}

Il tipo del primo parametro di un metodo di estensione indica il tipo a cui si applica l'estensione. Nell'esempio precedente il metodo di estensione Where estende il tipo IEnumerable<T>. Poiché Where è un metodo statico, è possibile richiamarlo direttamente come qualsiasi altro metodo statico:

IEnumerable<string> query = Enumerable.Where(names, 
                                          s => s.Length < 6);

Tuttavia, ciò che rende univoci i metodi di estensione è che possono anche essere richiamati usando la sintassi dell'istanza:

IEnumerable<string> query = names.Where(s => s.Length < 6);

I metodi di estensione vengono risolti in fase di compilazione in base ai metodi di estensione inclusi nell'ambito. Quando uno spazio dei nomi viene importato con un'istruzione using in C# o un'istruzione Import in Visual Basic, tutti i metodi di estensione definiti dalle classi statiche di tale spazio dei nomi vengono inseriti nell'ambito.

Gli operatori di query standard sono definiti come metodi di estensione nel tipo System.Linq.Enumerable. Quando si esaminano gli operatori di query standard, si noterà che tutti, ma alcuni di essi, sono definiti in termini di interfaccia T> IEnumerable<. Ciò significa che ogni origine informazioni compatibile con IEnumerable<T> ottiene gli operatori di query standard semplicemente aggiungendo l'istruzione using seguente in C#:

using System.Linq; // makes query operators visible

Gli utenti che desiderano sostituire gli operatori di query standard per un tipo specifico possono: definire metodi con lo stesso nome nel tipo specifico con firme compatibili o definire nuovi metodi di estensione con lo stesso nome che estendono il tipo specifico. Gli utenti che vogliono eschew gli operatori di query standard non possono semplicemente inserire System.Linq nell'ambito e scrivere i propri metodi di estensione per IEnumerable<T>.

Ai metodi di estensione viene assegnata la priorità più bassa in termini di risoluzione e vengono usati solo se non esiste alcuna corrispondenza appropriata per il tipo di destinazione e i relativi tipi di base. Ciò consente ai tipi definiti dall'utente di fornire i propri operatori di query che hanno la precedenza sugli operatori standard. Si consideri ad esempio la raccolta personalizzata seguente:

public class MySequence : IEnumerable<int> {
  public IEnumerator<int> GetEnumerator() {
    for (int i = 1; i <= 10; i++) 
      yield return i; 
  }

  IEnumerator IEnumerable.GetEnumerator() {
    return GetEnumerator(); 
  }

  public IEnumerable<int> Where(Func<int, bool> filter) {
    for (int i = 1; i <= 10; i++) 
      if (filter(i)) 
        yield return i;
  }
}

Dato questa definizione di classe, il programma seguente userà l'implementazione MySequence.Where , non il metodo di estensione, come i metodi di istanza hanno la precedenza sui metodi di estensione:

MySequence s = new MySequence();
foreach (int item in s.Where(n => n > 3))
    Console.WriteLine(item);

L'operatore OfType è uno dei pochi operatori di query standard che non estende un'origine di informazioni basata su IEnumerable<T>. Esaminiamo l'operatore di query OfType :

public static IEnumerable<T> OfType<T>(this IEnumerable source) {
  foreach (object item in source) 
    if (item is T) 
      yield return (T)item;
}

OfType accetta non solo origini basate su IEnumerable<>, ma anche origini scritte nell'interfaccia IEnumerable non parametrizzata presente nella versione 1.0 di .NET Framework. L'operatore OfType consente agli utenti di applicare gli operatori di query standard alle raccolte .NET classiche, come illustrato di seguito:

// "classic" cannot be used directly with query operators
IEnumerable classic = new OlderCollectionType();

// "modern" can be used directly with query operators
IEnumerable<object> modern = classic.OfType<object>();

In questo esempio la variabile restituisce modern la stessa sequenza di valori del classico. Tuttavia, il suo tipo è compatibile con il codice T> moderno IEnumerable<, inclusi gli operatori di query standard.

L'operatore OfType è utile anche per le origini informazioni più recenti, in quanto consente di filtrare i valori da un'origine in base al tipo. Quando si produce la nuova sequenza, OfType omette semplicemente i membri della sequenza originale che non sono compatibili con l'argomento di tipo. Si consideri questo semplice programma che estrae stringhe da una matrice eterogenea:

object[] vals = { 1, "Hello", true, "World", 9.1 };
IEnumerable<string> justStrings = vals.OfType<string>();

Quando si enumera la variabile justStrings in un'istruzione foreach , si otterrà una sequenza di due stringhe: "Hello" e "World".

Valutazione della query posticipata

I lettori osservanti potrebbero aver notato che l'operatore Where standard viene implementato usando il costrutto di rendimento introdotto in C# 2.0. Questa tecnica di implementazione è comune per tutti gli operatori standard che restituiscono sequenze di valori. L'uso del rendimento ha un vantaggio interessante, ovvero che la query non viene effettivamente valutata fino a quando non viene eseguita l'iterazione, con un'istruzione foreach o usando manualmente i metodi GetEnumerator e MoveNext sottostanti. Questa valutazione posticipata consente di mantenere le query come valori basati su T> IEnumerable< che possono essere valutati più volte, ogni volta che restituisce risultati potenzialmente diversi.

Per molte applicazioni, questo è esattamente il comportamento desiderato. Per le applicazioni che vogliono memorizzare nella cache i risultati della valutazione delle query, due operatori, ToList e ToArray, vengono forniti che forzano la valutazione immediata della query e restituiscono un elenco<T> o una matrice contenente i risultati della valutazione della query.

Per informazioni sul funzionamento della valutazione della query posticipata, considerare questo programma che esegue una semplice query su una matrice:

// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };

// declare a variable that represents a query
IEnumerable<string> ayes = names.Where(s => s[0] == 'A');

// evaluate the query
foreach (string item in ayes) 
  Console.WriteLine(item);

// modify the original information source
names[0] = "Bob";

// evaluate the query again, this time no "Allen"
foreach (string item in ayes) 
    Console.WriteLine(item);

La query viene valutata ogni volta che la variabile ayes viene iterazione. Per indicare che è necessaria una copia memorizzata nella cache dei risultati, è sufficiente aggiungere un operatore ToList o ToArray alla query come indicato di seguito:

// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };

// declare a variable that represents the result
// of an immediate query evaluation
string[] ayes = names.Where(s => s[0] == 'A').ToArray();

// iterate over the cached query results
foreach (string item in ayes) 
    Console.WriteLine(item);

// modifying the original source has no effect on ayes
names[0] = "Bob";

// iterate over result again, which still contains "Allen"
foreach (string item in ayes)
    Console.WriteLine(item);

Sia ToArray che ToList forzano la valutazione immediata delle query. Lo stesso vale per gli operatori di query standard che restituiscono valori singleton(ad esempio: First, ElementAt, Sum, Average, All, Any).

Interfaccia T> IQueryable<

Lo stesso modello di esecuzione posticipato è in genere desiderato per le origini dati che implementano la funzionalità di query usando alberi delle espressioni, ad esempio LINQ to SQL. Queste origini dati possono trarre vantaggio dall'implementazione dell'interfaccia IQueryable<T> per cui tutti gli operatori di query richiesti dal modello LINQ vengono implementati usando alberi delle espressioni. Ogni IQueryable<T> ha una rappresentazione di "codice necessario per eseguire la query" sotto forma di albero delle espressioni. Tutti gli operatori di query posticipati restituiscono un nuovo oggetto IQueryable<T> che aumenta l'albero delle espressioni con una rappresentazione di una chiamata a tale operatore di query. Pertanto, quando diventa tempo per valutare la query, in genere perché l'oggetto IQueryable<T> viene enumerato, l'origine dati può elaborare l'albero delle espressioni che rappresenta l'intera query in un batch. Ad esempio, una query LINQ to SQL complessa ottenuta da numerose chiamate agli operatori di query può comportare solo una singola query SQL inviata al database.

Il vantaggio per gli implementatori dell'origine dati di riutilizzare questa funzionalità di rinvio implementando l'interfaccia T IQueryable<T> è evidente. Per i client che scrivono le query, d'altra parte, è un grande vantaggio avere un tipo comune per le origini informazioni remote. Non solo consente loro di scrivere query polimorfiche che possono essere usate su origini diverse di dati, ma si apre anche la possibilità di scrivere query che passano tra domini.

Inizializzazione dei valori composti

Le espressioni lambda e i metodi di estensione forniscono tutti gli elementi necessari per le query che filtrano semplicemente i membri da una sequenza di valori. La maggior parte delle espressioni di query esegue anche la proiezione su tali membri, trasformando in modo efficace i membri della sequenza originale in membri il cui valore e tipo possono essere diversi dall'originale. Per supportare la scrittura di queste trasformazioni, LINQ si basa su un nuovo costrutto denominato inizializzatori di oggetti per creare nuove istanze di tipi strutturati. Per il resto di questo documento, si presuppone che il tipo seguente sia stato definito:

public class Person {
  string name;
  int age;
  bool canCode;

  public string Name {
    get { return name; } set { name = value; }
  }

  public int Age {
    get { return age; } set { age = value; }
  }

  public bool CanCode {
    get { return canCode; } set { canCode = value; }
  }
}

Gli inizializzatori a oggetti consentono di costruire facilmente valori in base ai campi pubblici e alle proprietà di un tipo. Ad esempio, per creare un nuovo valore di tipo Person, è possibile scrivere questa istruzione:

Person value = new Person {
    Name = "Chris Smith", Age = 31, CanCode = false
};

Semanticamente, questa istruzione equivale alla sequenza di istruzioni seguente:

Person value = new Person();
value.Name = "Chris Smith";
value.Age = 31;
value.CanCode = false;

Gli inizializzatori a oggetti sono una funzionalità importante per la query integrata nel linguaggio, in quanto consentono la costruzione di nuovi valori strutturati nei contesti in cui sono consentite solo espressioni, ad esempio all'interno di espressioni lambda e alberi delle espressioni. Si consideri, ad esempio, questa espressione di query che crea un nuovo valore Person per ogni valore della sequenza di input:

IEnumerable<Person> query = names.Select(s => new Person {
    Name = s, Age = 21, CanCode = s.Length == 5
});

La sintassi di inizializzazione degli oggetti è utile anche per inizializzare le matrici di valori strutturati. Si consideri ad esempio questa variabile di matrice inizializzata usando singoli inizializzatori di oggetti:

static Person[] people = {
  new Person { Name="Allen Frances", Age=11, CanCode=false },
  new Person { Name="Burke Madison", Age=50, CanCode=true },
  new Person { Name="Connor Morgan", Age=59, CanCode=false },
  new Person { Name="David Charles", Age=33, CanCode=true },
  new Person { Name="Everett Frank", Age=16, CanCode=true },
};

Valori e tipi strutturati

Il progetto LINQ supporta uno stile di programmazione incentrato sui dati in cui alcuni tipi esistono principalmente per fornire una "forma" statica su un valore strutturato anziché un oggetto full-blown con sia stato che comportamento. Prendendo questa premessa alla sua conclusione logica, è spesso il caso che tutti gli sviluppatori si preoccupano di è la struttura del valore e la necessità di un tipo denominato per tale forma è di poco uso. Ciò comporta l'introduzione di tipi anonimi che consentono di definire nuove strutture "inline" con la relativa inizializzazione.

In C#la sintassi per i tipi anonimi è simile alla sintassi di inizializzazione dell'oggetto, ad eccezione del fatto che il nome del tipo viene omesso. Si considerino, ad esempio, le due istruzioni seguenti:

object v1 = new Person {
    Name = "Brian Smith", Age = 31, CanCode = false
};

object v2 = new { // note the omission of type name
    Name = "Brian Smith", Age = 31, CanCode = false
};

Le variabili v1 e v2 puntano entrambi a un oggetto in memoria il cui tipo CLR ha tre proprietà pubbliche Name, Age e CanCode. Le variabili differiscono in quella v2 si riferisce a un'istanza di un tipo anonimo. In termini CLR, i tipi anonimi non sono diversi da qualsiasi altro tipo. Ciò che rende speciali i tipi anonimi è che non hanno alcun nome significativo nel linguaggio di programmazione. L'unico modo per creare istanze di un tipo anonimo consiste nell'usare la sintassi illustrata in precedenza.

Per consentire alle variabili di fare riferimento a istanze di tipi anonimi, ma ancora trarre vantaggio dalla digitazione statica, C# introduce variabili locali tipizzata in modo implicito: la parola chiave var può essere usata al posto del nome del tipo per le dichiarazioni di variabili locali. Si consideri ad esempio questo programma C# 3.0 legale:

var s = "Bob";
var n = 32;
var b = true;

La parola chiave var indica al compilatore di dedurre il tipo della variabile dal tipo statico dell'espressione usata per inizializzare la variabile. In questo esempio, i tipi di s, n e b sono rispettivamente string, int e bool. Questo programma è identico al seguente:

string s = "Bob";
int    n = 32;
bool   b = true;

La parola chiave var è una praticità per le variabili i cui tipi hanno nomi significativi, ma è una necessità per le variabili che fanno riferimento a istanze di tipi anonimi.

var value = new { 
  Name = " Brian Smith", Age = 31, CanCode = false
};

Nell'esempio precedente, il valore della variabile è di un tipo anonimo la cui definizione è equivalente alla pseudo-C#seguente:

internal class ??? {
  string _Name;
  int    _Age;
  bool   _CanCode;

  public string Name { 
    get { return _Name; } set { _Name = value; }
  }

  public int Age{ 
    get { return _Age; } set { _Age = value; }
  }

  public bool CanCode { 
    get { return _CanCode; } set { _CanCode = value; }
  }

  public bool Equals(object obj) { ... }

  public bool GetHashCode() { ... }
}

I tipi anonimi non possono essere condivisi tra limiti di assembly; tuttavia, il compilatore garantisce che all'interno di ogni assembly sia presente un tipo anonimo per una determinata sequenza di coppie nome/tipo di proprietà.

Poiché i tipi anonimi vengono spesso usati nelle proiezioni per selezionare uno o più membri di un valore strutturato esistente, è sufficiente fare riferimento a campi o proprietà da un altro valore nell'inizializzazione di un tipo anonimo. In questo modo il nuovo tipo anonimo ottiene una proprietà il cui nome, tipo e valore vengono copiati dalla proprietà o dal campo a cui si fa riferimento.

Si consideri, ad esempio, questo esempio che crea un nuovo valore strutturato combinando proprietà da altri valori:

var bob = new Person { Name = "Bob", Age = 51, CanCode = true };
var jane = new { Age = 29, FirstName = "Jane" };

var couple = new {
    Husband = new { bob.Name, bob.Age },
    Wife = new { Name = jane.FirstName, jane.Age }
};

int    ha = couple.Husband.Age; // ha == 51
string wn = couple.Wife.Name;   // wn == "Jane"

Il riferimento a campi o proprietà mostrati in precedenza è semplicemente una sintassi comoda per scrivere il modulo più esplicito seguente:

var couple = new {
    Husband = new { Name = bob.Name, Age = bob.Age },
    Wife = new { Name = jane.FirstName, Age = jane.Age }
};

In entrambi i casi, la variabile di coppia ottiene la propria copia delle proprietà Name and Age da bob e jane.

I tipi anonimi vengono spesso usati nella clausola select di una query. Ad esempio, si consideri la query seguente:

var query = people.Select(p => new { 
               p.Name, BadCoder = p.Age == 11
           });

foreach (var item in query) 
  Console.WriteLine("{0} is a {1} coder", 
                     item.Name,
                     item.BadCoder ? "bad" : "good");

In questo esempio è stato possibile creare una nuova proiezione sul tipo Person che corrisponde esattamente alla forma necessaria per il codice di elaborazione, ma ci ha ancora dato i vantaggi di un tipo statico.

Altri operatori di query standard

Oltre alle strutture di query di base descritte in precedenza, un numero di operatori offre modi utili per modificare sequenze e comporre query, dando all'utente un grado elevato di controllo sul risultato all'interno del framework pratico degli operatori di query standard.

Ordinamento e raggruppamento

In generale, la valutazione di una query genera una sequenza di valori generati in un ordine intrinseco nelle origini informazioni sottostanti. Per concedere agli sviluppatori il controllo esplicito sull'ordine in cui vengono prodotti questi valori, gli operatori di query standard vengono definiti per controllare l'ordine. La maggior parte di questi operatori è l'operatore OrderBy .

Gli operatori OrderBy e OrderByDescending possono essere applicati a qualsiasi origine informazioni e consentono all'utente di fornire una funzione di estrazione chiave che produce il valore utilizzato per ordinare i risultati. OrderBy e OrderByDescending accettano anche una funzione di confronto facoltativa che può essere usata per imporre un ordine parziale sulle chiavi. Si esaminerà un esempio di base:

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

// unity sort
var s1 = names.OrderBy(s => s); 
var s2 = names.OrderByDescending(s => s);

// sort by length
var s3 = names.OrderBy(s => s.Length); 
var s4 = names.OrderByDescending(s => s.Length);

Le prime due espressioni di query producono nuove sequenze basate sull'ordinamento dei membri dell'origine in base al confronto tra stringhe. Le seconde due query producono nuove sequenze basate sull'ordinamento dei membri dell'origine in base alla lunghezza di ogni stringa.

Per consentire più criteri di ordinamento, sia OrderBy che OrderByDescending restituiscono OrderSequence<T> anziché il valore IEnumerable T> generico<. Due operatori vengono definiti solo in OrderSequence<T>, ovvero ThenBy e ThenBy Decrescente, che applicano un criterio di ordinamento aggiuntivo (subordinato). QuindiBy/ThenBy Decrescente si restituiscono OrderSequence<T>, consentendo l'applicazione di qualsiasi numero di operatori ThenBy/ThenByDescending :

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var s1 = names.OrderBy(s => s.Length).ThenBy(s => s);

La valutazione della query a cui fa riferimento s1 in questo esempio restituisce la sequenza di valori seguente:

"Burke", "David", "Frank", 
"Albert", "Connor", "George", "Harris", 
"Everett"

Oltre alla famiglia di operatori OrderBy , gli operatori di query standard includono anche un operatore Inverso . Inverso enumera semplicemente su una sequenza e restituisce gli stessi valori in ordine inverso. A differenza di OrderBy, Reverse non considera i valori effettivi stessi per determinare l'ordine, invece si basa esclusivamente sull'ordine in cui i valori vengono generati dall'origine sottostante.

L'operatore OrderBy impone un ordinamento su una sequenza di valori. Gli operatori di query standard includono anche l'operatore GroupBy , che impone un partizionamento su una sequenza di valori in base a una funzione di estrazione delle chiavi. L'operatore GroupBy restituisce una sequenza di valori IGrouping , uno per ogni valore di chiave distinto rilevato. Un IGrouping è un IEnumerable che contiene anche la chiave usata per estrarre il contenuto:

public interface IGrouping<K, T> : IEnumerable<T> {
  public K Key { get; }
}

L'applicazione più semplice di GroupBy è simile alla seguente:

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

// group by length
var groups = names.GroupBy(s => s.Length);

foreach (IGrouping<int, string> group in groups) {
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (string value in group)
        Console.WriteLine("  {0}", value);
}    

Durante l'esecuzione, questo programma stampa quanto segue:

Strings of length 6
  Albert
  Connor
  George
  Harris
Strings of length 5
  Burke
  David
  Frank
Strings of length 7
  Everett

A la Select, GroupBy consente di fornire una funzione di proiezione usata per popolare i membri dei gruppi.

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

// group by length
var groups = names.GroupBy(s => s.Length, s => s[0]);
foreach (IGrouping<int, char> group in groups) {
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (char value in group)
        Console.WriteLine("  {0}", value);
}  

Questa variante stampa quanto segue:

Strings of length 6
  A
  C
  G
  H
Strings of length 5
  B
  D
  F
Strings of length 7
  E

Nota Da questo esempio il tipo proiettato non deve essere uguale all'origine. In questo caso è stato creato un raggruppamento di numeri interi a caratteri da una sequenza di stringhe.

Operatori di aggregazione

Diversi operatori di query standard sono definiti per l'aggregazione di una sequenza di valori in un singolo valore. L'operatore di aggregazione più generale è Aggregate, definito come segue:

public static U Aggregate<T, U>(this IEnumerable<T> source, 
                                U seed, Func<U, T, U> func) {
  U result = seed;

  foreach (T element in source) 
      result = func(result, element);

  return result;
}

L'operatore Aggregate semplifica l'esecuzione di un calcolo su una sequenza di valori. L'aggregazione funziona chiamando l'espressione lambda una volta per ogni membro della sequenza sottostante. Ogni volta che Aggregate chiama l'espressione lambda, passa sia il membro dalla sequenza che un valore aggregato (il valore iniziale è il parametro seed su Aggregate). Il risultato dell'espressione lambda sostituisce il valore aggregato precedente e Aggregate restituisce il risultato finale dell'espressione lambda.

Ad esempio, questo programma usa Aggregate per accumulare il numero totale di caratteri su una matrice di stringhe:

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

int count = names.Aggregate(0, (c, s) => c + s.Length);
// count == 46

Oltre all'operatore Aggregate per utilizzo generico, gli operatori di query standard includono anche un operatore Count per utilizzo generico e quattro operatori di aggregazione numerici (Min, Max, Sum e Average) che semplificano queste operazioni di aggregazione comuni. Le funzioni di aggregazione numerica funzionano su sequenze di tipi numerici (ad esempio , int, double, decimal) o su sequenze di valori arbitrari, purché venga fornita una funzione che proietta i membri della sequenza in un tipo numerico.

Questo programma illustra entrambe le forme dell'operatore Sum appena descritto:

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

int total1 = numbers.Sum();            // total1 == 55
int total2 = names.Sum(s => s.Length); // total2 == 46

Nota La seconda istruzione Sum equivale all'esempio precedente usando Aggregate.

Selezionare e selezionareMany

L'operatore Select richiede la funzione transform per produrre un valore per ogni valore nella sequenza di origine. Se la funzione di trasformazione restituisce un valore che è se stesso una sequenza, spetta al consumer attraversare manualmente le sotto-sequenze. Si consideri ad esempio questo programma che suddivide le stringhe in token usando il metodo String.Split esistente:

string[] text = { "Albert was here", 
                  "Burke slept late", 
                  "Connor is happy" };

var tokens = text.Select(s => s.Split(' '));

foreach (string[] line in tokens)
    foreach (string token in line)
        Console.Write("{0}.", token);

Quando viene eseguito, questo programma stampa il testo seguente:

Albert.was.here.Burke.slept.late.Connor.is.happy.

Idealmente, sarebbe preferibile che la query abbia restituito una sequenza di token coalesced e non abbia esposto la stringa intermedia[] al consumer. A tale scopo, viene usato l'operatore SelectMany anziché l'operatore Select . L'operatore SelectMany funziona in modo analogo all'operatore Select . È diverso dal fatto che la funzione di trasformazione restituisce una sequenza che viene quindi espansa dall'operatore SelectMany . Ecco il programma riscritto usando SelectMany:

string[] text = { "Albert was here", 
                  "Burke slept late", 
                  "Connor is happy" };

var tokens = text.SelectMany(s => s.Split(' '));

foreach (string token in tokens)
    Console.Write("{0}.", token);

L'uso di SelectMany fa sì che ogni sequenza intermedia venga espansa come parte della valutazione normale.

SelectMany è ideale per combinare due origini informazioni:

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.SelectMany(n => 
                     people.Where(p => n.Equals(p.Name))
                 );

Nell'espressione lambda passata a SelectMany, la query nidificata si applica a un'origine diversa, ma ha nell'ambito il n parametro passato dall'origine esterna. Così la gente. Dove viene chiamato una volta per ogni n, con le sequenze risultanti appiattite da SelectMany per l'output finale. Il risultato è una sequenza di tutte le persone il cui nome viene visualizzato nella matrice dei nomi .

Operatori di join

In un programma orientato a oggetti, gli oggetti correlati tra loro verranno in genere collegati con riferimenti a oggetti facili da esplorare. Lo stesso vale in genere per le origini informazioni esterne, in cui le voci di dati spesso non hanno alcuna opzione, ma "puntano" tra loro simbolicamente, con ID o altri dati che possono identificare in modo univoco l'entità a cui punta. Il concetto di join si riferisce all'operazione di riunire gli elementi di una sequenza con gli elementi con cui "corrispondono" da un'altra sequenza.

L'esempio precedente con SelectMany esegue esattamente questa operazione, associando stringhe con persone i cui nomi sono quelle stringhe. Tuttavia, per questo particolare scopo, l'approccio SelectMany non è molto efficiente, ma scorrerà tutti gli elementi delle persone per ogni elemento dei nomi. Raggruppando tutte le informazioni di questo scenario, le due origini informazioni e le "chiavi" con cui sono abbinate, insieme in una chiamata al metodo, l'operatore Join è in grado di eseguire un lavoro molto migliore:

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.Join(people, n => n, p => p.Name, (n,p) => p);

Si tratta di un po ' di un boccone, ma vedere come i pezzi si adattano: il metodo Join viene chiamato sull'origine dati "esterna", nomi. Il primo argomento è l'origine dati "interna", persone. Il secondo e il terzo argomento sono espressioni lambda per estrarre le chiavi dagli elementi delle origini esterne e interne, rispettivamente. Queste chiavi sono ciò che il metodo Join usa per trovare le corrispondenze con gli elementi. In questo caso si vuole che i nomi corrispondano alla proprietà Name delle persone. L'espressione lambda finale è quindi responsabile della produzione degli elementi della sequenza risultante: viene chiamata con ogni coppia di elementi corrispondenti n e p e viene usata per modellare il risultato. In questo caso si sceglie di eliminare n e restituire il valore p. Il risultato finale è l'elenco di elementi Person delle persone il cui Nome è nell'elenco dei nomi.

Un cugino più potente di Join è l'operatore GroupJoin . GroupJoin differisce da Join nel modo in cui viene usata l'espressione lambda di data shaping dei risultati: anziché essere richiamata con ogni singola coppia di elementi esterni e interni, viene chiamata una sola volta per ogni elemento esterno, con una sequenza di tutti gli elementi interni che corrispondono a tale elemento esterno. Per fare questo concreto:

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.GroupJoin(people, n => n, p => p.Name,                   
                 (n, matching) => 
                      new { Name = n, Count = matching.Count() }
);

Questa chiamata produce una sequenza dei nomi avviati con l'associazione al numero di persone che hanno tale nome. Di conseguenza, l'operatore GroupJoin consente di basare i risultati sull'intero "set di corrispondenze" per un elemento esterno.

Sintassi di query

L'istruzione foreach esistente in C# fornisce una sintassi dichiarativa per l'iterazione sui metodi IEnumerable/IEnumerator di .NET Framework. L'istruzione foreach è strettamente facoltativa, ma ha dimostrato di essere un meccanismo linguistico molto pratico e popolare.

Basandosi su questo precedente, le espressioni di query semplificano le query con una sintassi dichiarativa per gli operatori di query più comuni: Where, Join, GroupJoin, Select, SelectMany, GroupBy, OrderBy, ThenBy, OrderByDescending, ThenByDescending e Cast.

Si inizierà esaminando la semplice query con cui è stato avviato questo documento:

IEnumerable<string> query = names 
                            .Where(s => s.Length == 5) 
                            .OrderBy(s => s)
                            .Select(s => s.ToUpper());

Usando un'espressione di query è possibile riscrivere questa istruzione esatta come segue:

IEnumerable<string> query = from s in names 
                            where s.Length == 5
                            orderby s
                            select s.ToUpper();

Analogamente all'istruzione foreach in C#, le espressioni di query sono più compatte e facili da leggere, ma sono completamente facoltative. Ogni espressione che può essere scritta come espressione di query ha una sintassi corrispondente (anche se più dettagliata) usando la notazione dei punti.

Si inizierà esaminando la struttura di base di un'espressione di query. Ogni espressione di query sintattica in C# inizia con una clausola from e termina con una clausola select o group . La clausola initial from può essere seguita da zero o più clausole from, let, where, join e orderby . Ogni clausola from è un generatore che introduce una variabile di intervallo su una sequenza; ogni clausola let assegna un nome al risultato di un'espressione; e ogni clausola where è un filtro che esclude gli elementi dal risultato. Ogni clausola join correla una nuova origine dati con i risultati delle clausole precedenti. Una clausola orderby specifica un ordinamento per il risultato:

query-expression ::= from-clause query-body

query-body ::= 

      query-body-clause* final-query-clause query-continuation?

query-body-clause ::=
 (from-clause 
      | join-clause 
      | let-clause 
      | where-clause 
      | orderby-clause)

from-clause ::=from itemName in srcExpr

join-clause ::=join itemName in srcExpr on keyExpr equals keyExpr 
       (into itemName)?

let-clause ::=let itemName = selExpr

where-clause ::= where predExpr

orderby-clause ::= orderby (keyExpr (ascending | descending)?)*

final-query-clause ::=
 (select-clause | groupby-clause)

select-clause ::= select selExpr

groupby-clause ::= group selExpr by keyExprquery-continuation ::= intoitemName query-body

Si considerino, ad esempio, queste due espressioni di query:

var query1 = from p in people
             where p.Age > 20
             orderby p.Age descending, p.Name
             select new { 
                 p.Name, Senior = p.Age > 30, p.CanCode
             };

var query2 = from p in people
             where p.Age > 20
             orderby p.Age descending, p.Name
             group new { 
                p.Name, Senior = p.Age > 30, p.CanCode
             } by p.CanCode;

Il compilatore considera queste espressioni di query come se fossero scritte usando la notazione punto esplicita seguente:

var query1 = people.Where(p => p.Age > 20)
                   .OrderByDescending(p => p.Age)
                   .ThenBy(p => p.Name)
                   .Select(p => new { 
                       p.Name, 
                       Senior = p.Age > 30, 
                       p.CanCode
                   });

var query2 = people.Where(p => p.Age > 20)
                   .OrderByDescending(p => p.Age)
                   .ThenBy(p => p.Name)
                   .GroupBy(p => p.CanCode, 
                            p => new {
                                   p.Name, 
                                   Senior = p.Age > 30, 
                                   p.CanCode
                   });

Le espressioni di query vengono sottoposte a una conversione meccanica in chiamate di metodi con nomi specifici. L'implementazione esatta dell'operatore di query scelta dipende quindi sia dal tipo delle variabili sottoposte a query sia dai metodi di estensione inclusi nell'ambito.

Le espressioni di query mostrate finora hanno usato un solo generatore. Quando si utilizzano più generatori, ogni generatore successivo viene valutato nel contesto del predecessore. Si consideri ad esempio questa leggera modifica alla query:

var query = from s1 in names 
            where s1.Length == 5
            from s2 in names 
            where s1 == s2
            select s1 + " " + s2;

Quando viene eseguito su questa matrice di input:

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

si ottengono i risultati seguenti:

Burke Burke
Frank Frank
David David

L'espressione di query precedente si espande a questa espressione con notazione punto:

var query = names.Where(s1 => s1.Length == 5)
                 .SelectMany(s1 => names, (s1,s2) => new {s1,s2})
                 .Where($1 => $1.s1 == $1.s2) 
                 .Select($1 => $1.s1 + " " + $1.s2);

Nota Questa versione di SelectMany accetta un'espressione lambda aggiuntiva usata per produrre il risultato in base agli elementi delle sequenze esterne e interne. In questa espressione lambda le due variabili di intervallo vengono raccolte in un tipo anonimo. Il compilatore inventa un nome di variabile $1 per indicare che il tipo anonimo nelle espressioni lambda successive.

Un tipo speciale di generatore è la clausola join , che introduce elementi di un'altra origine che corrispondono agli elementi delle clausole precedenti in base alle chiavi fornite. Una clausola join può restituire gli elementi corrispondenti uno per uno, ma se specificato con una clausola into , gli elementi corrispondenti verranno assegnati come gruppo:

var query = from n in names
            join p in people on n equals p.Name into matching
            select new { Name = n, Count = matching.Count() };

Questa query non è sorprendentemente espansa direttamente in una vista precedente:

var query = names.GroupJoin(people, n => n, p => p.Name,                   
           (n, matching) => 
                      new { Name = n, Count = matching.Count() }
);

Spesso è utile considerare i risultati di una query come generatore in una query successiva. A tale scopo, le espressioni di query usano la parola chiave into per complice una nuova espressione di query dopo una clausola select o group. Si tratta di una continuazione di query.

La parola chiave into è particolarmente utile per la post-elaborazione dei risultati di una clausola group by . Si consideri ad esempio questo programma:

var query = from item in names
            orderby item
            group item by item.Length into lengthGroups
            orderby lengthGroups.Key descending
            select lengthGroups;

foreach (var group in query) { 
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (var val in group)
        Console.WriteLine("  {0}", val);
}

Questo programma restituisce quanto segue:

Strings of length 7
  Everett
Strings of length 6
  Albert
  Connor
  George
  Harris
Strings of length 5
  Burke
  David
  Frank

In questa sezione è stato descritto come C# implementa le espressioni di query. Altri linguaggi possono scegliere di supportare operatori di query aggiuntivi con sintassi esplicita o di non disporre di espressioni di query.

È importante notare che la sintassi della query non è cablata in modo rigido agli operatori di query standard. Si tratta di una funzionalità puramente sintattica che si applica a qualsiasi elemento che soddisfi il modello di query implementando i metodi sottostanti con i nomi e le firme appropriati. Gli operatori di query standard descritti in precedenza usano metodi di estensione per aumentare l'interfaccia T> IEnumerable<. Gli sviluppatori possono sfruttare la sintassi di query su qualsiasi tipo desiderato, purché si assicurino che rispetti il modello di query, tramite l'implementazione diretta dei metodi necessari o aggiungendoli come metodi di estensione.

Questa estendibilità viene sfruttata nel progetto LINQ stesso tramite il provisioning di due API abilitate per LINQ, ovvero LINQ to SQL, che implementa il modello LINQ per l'accesso ai dati basato su SQL e LINQ to XML che consente query LINQ sui dati XML. Entrambe queste sono descritte nelle sezioni seguenti.

LINQ to SQL: Integrazione SQL

.NET Language-Integrated Query può essere usata per eseguire query su archivi dati relazionali senza uscire dalla sintassi o dall'ambiente in fase di compilazione del linguaggio di programmazione locale. Questa funzionalità, denominata LINQ to SQL, sfrutta l'integrazione delle informazioni sullo schema SQL nei metadati CLR. Questa integrazione compila le definizioni di tabella e vista SQL in tipi CLR a cui è possibile accedere da qualsiasi linguaggio.

LINQ to SQL definisce due attributi principali, [Table] e [Column], che indicano quali tipi e proprietà CLR corrispondono a dati SQL esterni. L'attributo [Table] può essere applicato a una classe e associa il tipo CLR a una tabella o vista SQL denominata. L'attributo [Column] può essere applicato a qualsiasi campo o proprietà e associa il membro a una colonna SQL denominata. Entrambi gli attributi sono parametrizzati per consentire la conservazione di metadati specifici di SQL. Si consideri ad esempio questa semplice definizione di schema SQL:

create table People (
    Name nvarchar(32) primary key not null, 
    Age int not null, 
    CanCode bit not null
)

create table Orders (
    OrderID nvarchar(32) primary key not null, 
    Customer nvarchar(32) not null, 
    Amount int
)

L'equivalente CLR è simile al seguente:

[Table(Name="People")]
public class Person {
  [Column(DbType="nvarchar(32) not null", Id=true)]
  public string Name; 

  [Column]
  public int Age;

  [Column]
  public bool CanCode;
}

[Table(Name="Orders")]
public class Order {
  [Column(DbType="nvarchar(32) not null", Id=true)]
  public string OrderID; 

  [Column(DbType="nvarchar(32) not null")]        
  public string Customer; 

  [Column]
  public int? Amount; 
}

Nota In questo esempio le colonne nullable eseguono il mapping a tipi nullable in CLR (tipi nullable sono apparsi per la prima volta nella versione 2.0 di .NET Framework) e che per i tipi SQL che non hanno una corrispondenza 1:1 con un tipo CLR (ad esempio, nvarchar, char, text), il tipo SQL originale viene mantenuto nei metadati CLR.

Per eseguire una query su un archivio relazionale, l'implementazione LINQ to SQL del modello LINQ converte la query dal formato dell'albero delle espressioni in un'espressione SQL e ADO.NET oggetto DbCommand adatto per la valutazione remota. Si consideri ad esempio questa semplice query:

// establish a query context over ADO.NET sql connection
DataContext context = new DataContext(
     "Initial Catalog=petdb;Integrated Security=sspi");

// grab variables that represent the remote tables that 
// correspond to the Person and Order CLR types
Table<Person> custs = context.GetTable<Person>();
Table<Order> orders   = context.GetTable<Order>();

// build the query
var query = from c in custs
            from o in orders
            where o.Customer == c.Name
            select new { 
                       c.Name, 
                       o.OrderID,
                       o.Amount,
                       c.Age
            }; 

// execute the query
foreach (var item in query) 
    Console.WriteLine("{0} {1} {2} {3}", 
                      item.Name, item.OrderID, 
                      item.Amount, item.Age);

Il tipo DataContext fornisce un traduttore leggero che converte gli operatori di query standard in SQL. DataContext usa il ADO.NET IDbConnection esistente per l'accesso all'archivio e può essere inizializzato con un oggetto connessione ADO.NET stabilito o una stringa di connessione che può essere usata per crearne una.

Il metodo GetTable fornisce variabili compatibili con IEnumerable che possono essere usate nelle espressioni di query per rappresentare la tabella o la vista remota. Le chiamate a GetTable non causano alcuna interazione con il database, ma rappresentano il potenziale di interagire con la tabella o la vista remota usando espressioni di query. Nell'esempio precedente la query non viene trasmessa all'archivio finché il programma non esegue l'iterazione sull'espressione di query, in questo caso usando l'istruzione foreach in C#. Quando il programma esegue per la prima volta l'iterazione della query, il macchinario DataContext converte l'albero delle espressioni nell'istruzione SQL seguente inviata all'archivio:

SELECT [t0].[Age], [t1].[Amount], 
       [t0].[Name], [t1].[OrderID]
FROM [Customers] AS [t0], [Orders] AS [t1]
WHERE [t1].[Customer] = [t0].[Name]

È importante notare che creando funzionalità di query direttamente nel linguaggio di programmazione locale, gli sviluppatori ottengono tutta la potenza del modello relazionale senza dover creare in modo statico le relazioni nel tipo CLR. Detto questo, il mapping completo di oggetti/relazionali può anche sfruttare questa funzionalità di query di base per gli utenti che desiderano tale funzionalità. LINQ to SQL fornisce funzionalità di mapping relazionale a oggetti con cui lo sviluppatore può definire e esplorare le relazioni tra oggetti. È possibile fare riferimento a Orders come proprietà della classe Customer usando il mapping, in modo che non siano necessari join espliciti per collegare i due elementi. I file di mapping esterni consentono di separare il mapping dal modello a oggetti per funzionalità di mapping più avanzate.

LINQ to XML: Integrazione XML

.NET Language-Integrated Query for XML (LINQ to XML) consente di eseguire query sui dati XML usando gli operatori di query standard, nonché gli operatori specifici dell'albero che forniscono una navigazione simile a XPath tra discendenti, predecessori e elementi di pari livello. Fornisce una rappresentazione efficiente in memoria per XML che si integra con l'infrastruttura lettore /writerSystem.Xmlesistente ed è più facile da usare rispetto al DOM W3C. Esistono tre tipi che eseguono la maggior parte del lavoro di integrazione di XML con query: XName, XElement e XAttribute.

XName offre un modo facile da usare per gestire gli identificatori qualificati per lo spazio dei nomi (QNames) usati sia come nomi di elemento che di attributo. XName gestisce in modo trasparente l'atomizzazione efficiente degli identificatori e consente l'uso di simboli o stringhe semplici ovunque sia necessario un QName.

Gli elementi e gli attributi XML vengono rappresentati rispettivamente usando XElement e XAttribute . XElement e XAttribute supportano la sintassi di costruzione normale, consentendo agli sviluppatori di scrivere espressioni XML usando una sintassi naturale:

var e = new XElement("Person", 
                     new XAttribute("CanCode", true),
                     new XElement("Name", "Loren David"),
                     new XElement("Age", 31));

var s = e.ToString();

Corrisponde al codice XML seguente:

<Person CanCode="true">
  <Name>Loren David</Name> 
  <Age>31</Age> 
</Person>

Si noti che non è stato necessario alcun modello factory basato su DOM per creare l'espressione XML e che l'implementazione ToString ha restituito il codice XML testuale. Gli elementi XML possono essere costruiti anche da un XmlReader esistente o da un valore letterale stringa:

var e2 = XElement.Load(xmlReader);
var e1 = XElement.Parse(
@"<Person CanCode='true'>
  <Name>Loren David</Name>
  <Age>31</Age>
</Person>");

XElement supporta anche la creazione di codice XML usando il tipo XmlWriter esistente.

Le code XElement con gli operatori di query consentono agli sviluppatori di scrivere query su informazioni non XML e produrre risultati XML creando XElements nel corpo di una clausola select:

var query = from p in people 
            where p.CanCode
            select new XElement("Person", 
                                  new XAttribute("Age", p.Age),
                                  p.Name);

Questa query restituisce una sequenza di XElements. Per consentire la compilazione di XElements dal risultato di questo tipo di query, il costruttore XElement consente di passare direttamente le sequenze di elementi come argomenti:

var x = new XElement("People",
                  from p in people 
                  where p.CanCode
                  select 
                    new XElement("Person", 
                                   new XAttribute("Age", p.Age),
                                   p.Name));

Questa espressione XML restituisce il codice XML seguente:

<People>
  <Person Age="11">Allen Frances</Person> 
  <Person Age="59">Connor Morgan</Person> 
</People>

L'istruzione precedente ha una traduzione diretta in Visual Basic. Tuttavia, Visual Basic 9.0 supporta anche l'uso di valori letterali XML, che consentono di esprimere le espressioni di query usando una sintassi XML dichiarativa direttamente da Visual Basic. L'esempio precedente può essere costruito con l'istruzione Visual Basic:

 Dim x = _
        <People>
             <%= From p In people __
                 Where p.CanCode _

                 Select <Person Age=<%= p.Age %>>p.Name</Person> _
             %>
        </People>

Gli esempi finora hanno illustrato come costruire nuovi valori XML usando la query integrata nel linguaggio. I tipi XElement e XAttribute semplificano anche l'estrazione di informazioni dalle strutture XML. XElement fornisce metodi di accesso che consentono l'applicazione delle espressioni di query agli assi XPath tradizionali. Ad esempio, la query seguente estrae solo i nomi da XElement illustrati in precedenza:

IEnumerable<string> justNames =
    from e in x.Descendants("Person")
    select e.Value;

//justNames = ["Allen Frances", "Connor Morgan"]

Per estrarre valori strutturati dal codice XML, è sufficiente usare un'espressione di inizializzatore di oggetti nella clausola select:

IEnumerable<Person> persons =
    from e in x.Descendants("Person")
    select new Person { 
        Name = e.Value,
        Age = (int)e.Attribute("Age") 
    };

Si noti che XAttribute e XElement supportano conversioni esplicite per estrarre il valore di testo come tipo primitivo. Per gestire i dati mancanti, è sufficiente eseguire il cast a un tipo nullable:

IEnumerable<Person> persons =
    from e in x.Descendants("Person")
    select new Person { 
        Name = e.Value,
        Age = (int?)e.Attribute("Age") ?? 21
    };

In questo caso, viene usato un valore predefinito pari a 21 quando l'attributo Age non è presente.

Visual Basic 9.0 offre il supporto del linguaggio diretto per i metodi della funzione di accesso Elements, Attribute e Descendants di XElement, consentendo l'accesso ai dati basati su XML usando una sintassi più compatta e diretta denominata proprietà dell'asse XML. È possibile usare questa funzionalità per scrivere l'istruzione C# precedente come segue:

Dim persons = _
      From e In x...<Person> _   
      Select new Person { _
          .Name = e.Value, _
          .Age = IIF(e.@Age, 21) _
      } 

In Visual Basic, x...<Person> ottiene tutti gli elementi nell'insieme Descendants di x con il nome Person, mentre l'espressione e.@Age trova tutti gli XAttributes con il nome Age. The Value ottiene il primo attributo nell'insieme e chiama la proprietà Value su tale attributo.

Riepilogo

.NET Language-Integrated Query aggiunge funzionalità di query a CLR e ai linguaggi di destinazione. La funzionalità di query si basa su espressioni lambda e alberi delle espressioni per consentire l'uso di predicati, proiezioni ed espressioni di estrazione delle chiavi come codice eseguibile opaco o come dati trasparenti in memoria adatti per l'elaborazione o la traduzione downstream. Gli operatori di query standard definiti dal progetto LINQ funzionano su qualsiasi origine informazioni basata su T> IEnumerable< e sono integrati con ADO.NET (LINQ to SQL) e System.Xml (LINQ to XML) per consentire ai dati relazionali e XML di ottenere i vantaggi della query integrata nel linguaggio.

Operatori di query standard in una nutshell

Operatore Descrizione
Where Operatore di restrizione basato sulla funzione predicato
Seleziona/SelezionaGestisci Operatori di proiezione basati sulla funzione selettore
Take/Skip/ TakeWhile/SkipWhileWhile Operatori di partizionamento basati sulla funzione di posizione o predicato
Join/GroupJoin Operatori di join basati sulle funzioni del selettore chiave
Concat Concatenation - operatore
OrderBy/ThenBy/OrderByDescending/ThenByDescending Ordinamento degli operatori in ordine crescente o decrescente in base alle funzioni facoltative del selettore di chiavi e dell'operatore di confronto
Reverse Operatore di ordinamento che ripristina l'ordine di una sequenza
GroupBy Operatore di raggruppamento basato su funzioni facoltative del selettore di chiavi e di confronto
Distinct Operatore Set che rimuove i duplicati
Unione/Intersect Impostare gli operatori che restituiscono l'unione o l'intersezione dei set
Except Operatore set che restituisce la differenza set
AsEnumerable Operatore di conversione in IEnumerable<T>
ToArray/ToList Operatore di conversione in matrice o elenco<T>
ToDictionary/ToLookup Operatori di conversione in Dizionario<K, T> o Ricerca<K,T> (multi-dizionario) in base alla funzione del selettore di chiavi
OfType/Cast Operatori di conversione in IEnumerable<T> in base al filtro in base o conversione in base all'argomento di tipo
SequenceEqual Operatore di uguaglianza che controlla l'uguaglianza degli elementi pairwise
First/FirstOrDefault/Last/LastOrDefault/Single/SingleOrDefault Operatori di elemento che restituiscono l'elemento initial/final/only in base alla funzione di predicato facoltativa
ElementAt/ElementAtOrDefault Operatori elemento che restituiscono elementi in base alla posizione
DefaultIfEmpty Operatore element che sostituisce una sequenza vuota con sequenza singleton con valori predefiniti
Intervallo Operatore di generazione che restituisce numeri in un intervallo
Repeat Operatore di generazione che restituisce più occorrenze di un determinato valore
Empty Operatore di generazione che restituisce una sequenza vuota
Tutto Controllo quantificatore per la soddisfazione esistenziale o universale della funzione predicato
Contiene Controllo del quantificatore per la presenza di un elemento specificato
Conteggio/LongCount Conteggio degli operatori di aggregazione in base alla funzione predicato facoltativa
Somma/Min/Max/Media Operatori di aggregazione basati sulle funzioni del selettore facoltativo
Aggregate Operatore di aggregazione accumulando più valori in base alla funzione di accumulo e al seeding facoltativo