LINQ è l'acronimo di Language INtegrated Query, ed è una delle novità del .NET Framework 3.5. Rappresenta il primo framework Microsoft per l'accesso ai dati, indipendente dall'architettura e dalle strutture cui si tenta di accedere e totalmente integrato all'interno dei linguaggi .NET di alto livello.
Con LINQ possiamo eseguire query e manipolare dati sfruttando un modello indipendente dalle varie tipologie di fonti; possiamo infatti accedere a database, file di testo, file XML, array, file Excel, file di configurazione, informazioni su assembly, chiavi di registro e qualsiasi altro oggetto riconducibile ad una collezione di oggetti enumerabile; il tutto utilizzando un unico modello di programmazione che riunisce molteplici tecniche differenti di accesso ai dati.
Per il corretto funzionamento di questo nuovo linguaggio, sono state aggiunte nuove funzionalità ai linguaggi del .NET Framework 3.5, in grado di supportare l'esecuzione delle query.
Sia per C# che per VB.NET abbiamo a disposizione nuove keyword, il meccanismo delle variabili locali implicite e dei tipi anonimi, gli inizializzatori di oggetti e collezioni, gli extension methods e le lambda expression. Abbiamo già esaminato queste novità in questo articolo.
LINQ porta con sé una serie di nuovi tool e funzionalità per migliorare la produttività. Il framework a supporto dell'accesso ai dati permette il mapping degli oggetti di SQL Server (Tabelle, viste, stored, etc.) su classi .NET (un vero e proprio ORM) ed è stata creata una sintassi particolare per accedere tramite LINQ a informazioni in formato XML e a strutture come DataSet
e DataTable
.
Le funzionalità appena descritte sono state fatte confluire in queste quattro implementazioni di LINQ:
- LINQ to Objects - permette di eseguire delle query su collezioni di oggetti in memoria
- LINQ to XML - permette di eseguire delle operazioni su informazioni in formato XML
- LINQ to DataSet - permette di eseguire delle query su DataSet tipizzati
- LINQ to SQL - permette di rappresentare un grafo di oggetti in memoria che rappresentano gli oggetti presenti in un database SQL Server, su cui poi eseguire delle query
L'insieme di queste implementazioni fornisce un potente strumento per la gestione dei dati in memoria, derivati da varie fonti diverse fra loro.
Sono ancora in fase di sviluppo (Beta 3) LINQ to Entities e Entity Framework, grazie ai quali si ottiene un mapping più potente e funzionale e la gestione di numerosi DBMS. Avremo sicuramente modo di parlare in dettaglio anche di questa novità.
La prima query
Come primo esempio, vediamo come recuperare tutti i numeri pari da un array di interi. La query è molto semplice, ma permette perfettamente di vedere la nuova sintassi LINQ e una piccola parte delle nuove keyword disponibili.
Query per i numeri pari (C#)
int[] numeri = { 1, 2, 3, 4, 5 };
var numeriPari = from n in numeri where (n%2 == 0) select n;
foreach (var item in numeriPari)
Console.WriteLine("{0}", item);
Query per i numeri pari (VB.NET)
Dim numeri As Integer() = New Integer() {1, 2, 3, 4, 5}
Dim numeriPari = From n In numeri Where (n Mod 2 = 0) Select n
For Each item In numeriPari
Console.WriteLine("{0}", item)
Next
La sintassi LINQ è molto simile a quella utilizzata per accedere ad una qualsiasi base di dati, ma è molto importante chiarire un concetto: LINQ non è solo un tool per utilizzare SQL.
Nell'esempio proposto abbiamo una fonte di dati rappresentata da un array di interi predefinito, una variabile implicita che rappresenta il risultato della nostra query di selezione (la variabile numeriPari
) e la query vera e propria che seleziona i numeri pari (utilizzando la variabile n
come valore generico della collezione di oggetti su cui effettuare il controllo tramite la clausola where
).
Proprio per permettere la scrittura di query LINQ sono state introdotte in entrambi i linguaggi queste nuove keyword:
from
- è la keyword di inizio di ogni query LINQ e specifica la fonte di dati nella quale dovrà essere eseguita la query.where
- è la clausola che specifica quali elementi saranno ritornati dalla query; applica una sorta di filtro di selezione.select
- è la clausola che definisce i tipi di valori che saranno prodotti dalla query.group
(Group By
in VB.NET) - è la clausola che raggruppa i risultati secondo una certa chiave.orderby
(Order By
in VB.NET) - effettua l'ordinamento (ascendente o discendente).join
- permette di effettuare prodotti cartesiani tra più fonti di dati, come in SQL. Anche qui possiamo definire inner join o outer join.into
(valida solo per C#) - è la keyword contestuale che indica in quale variabile temporanea vengono salvati i risultati di una select, di un group o di un join.let
(valida solo per C#) - è la keyword che permette di salvare temporaneamente il risultato di una subquery per poi utilizzarlo all'interno della query principale.Take
(valida solo per VB.NET) - clausola che ritorna il numero specifico dei numeri contigui dall'inizio di una collezione.Distinct
- clausola che restringe il numero dei valori ritornati da una query eliminando i duplicati (valida solo per VB.NET ma in C# si può usare il metodoDistinct()
).
Grazie alle modifiche sui linguaggi del .NET Framework e alle aggiunte funzionali come le variabili implicite (nel nostro esempio "numeriPari" risulta una variabile che implicitamente viene dichiarata di tipo IEnumerable
), possiamo utilizzare LINQ per effettuare rapidamente operazioni, su collezioni di oggetti, che prima avrebbero richiesto molte righe di codice, cicli e condizioni di controllo.
Nell'esempio appena visto, abbiamo effettuato una selezione su una collezione di oggetti in memoria; questa funzionalità fa parte dell'implementazione "LINQ to Objects" che ci permette di eseguire specifiche operazioni sulle collezioni di oggetti. Questa implementazione è la base di tutte le query LINQ e viene abilitata non appena viene inserito il namespace System.Linq
nelle direttive using
(o Imports
per VB.NET).
Se invece, volessimo selezionare da un array di stringhe tutte le parole che iniziano con una particolare lettera ed infine ordinare i risultati in ordine ascendente, potremmo usare una query LINQ di questo genere:
Nomi che iniziano per 'P' (C#)
string[] nomi = { "Peppe", "Andrea", "Paola", "Patrizia" }; var nomiCheInizianoConLaP = from n in nomi where n.StartsWith("P") orderby n ascending select n;
Nomi che iniziano per 'P' (VB.NET)
Dim nomi As String() = New String() {"Peppe", "Andrea", "Paola", "Patrizia"} Dim nomiCheInizianoConLaP = From n In nomi _ Where n.StartsWith("P") _ Order By n Ascending _ Select n
Utilizzando il metodo StartsWith()
della classe String
e la nuova keyword orderby
, riusciamo a raggiungere il nostro obbiettivo in sole 2 righe di codice. Prima dell'arrivo di LINQ, tale operazione poteva essere eseguita in questo modo:
Nomi che iniziano per 'P' (C# senza LINQ)
string[] nomi = { "Peppe", "Andrea", "Paolo", "Patrizia" }; List<string> nomiCheInizianoConLaP = new List<string>(); foreach (string s in nomi) if (s.StartsWith("P")) nomiCheInizianoConLaP.Add(s); nomiCheInizianoConLaP.Sort();
LINQ to XML
L'implementazione di LINQ per la gestione di dati in formato XML ci consente di compiere le normali operazioni di selezione, creazione e cancellazione di nodi, ricerca di valori, creazione di nuovi documenti e modifiche a documenti già esistenti (chiaramente in formato XML) utilizzando una sintassi molto chiara e che diminuisce notevolmente il numero di righe di codice da scrivere.
LINQ to XML è un vero e proprio framework per l'accesso e la gestione delle informazioni in formato XML con completo supporto sia alle funzionalità DOM che a quelle definite dai linguaggi XPath e XQuery, il tutto integrato all'interno di un modello unico di programmazione basato sulla nuova sintassi LINQ e sulle nuove funzionalità inserite all'interno dei linguaggi di programmazione di alto livello della nuova versione del .NET Framework.
Le operazioni che siamo in grado di compiere attraverso questo nuovo modello sono più o meno le stesse messe a disposizione dal namespace System.Xml
, con la differenza però che queste operazioni vengono esplicitate attraverso una nuova sintassi, più semplice ed intuitiva. Con LINQ to XML, possiamo infatti:
- caricare informazioni in formato XML in memoria.
- Creare nuovi alberi XML.
- Inserire, modificare o cancellare nodi da alberi XML in memoria.
- Salvare le informazioni create o gestite in memoria su file.
Il namespace di riferimento per queste nuove funzionalità è System.Xml.Linq
, che contiene nuove classi per rappresentare documenti, elementi, attributi, nomi e in generale informazioni presenti nei documenti XML e per mantenere tali dati in memoria.
La novità che salta subito all'occhio utilizzando queste nuove classi è che possiamo rappresentare ogni singolo oggetto di un documento XML, senza dover creare il contesto che rappresenta l'intero documento XML, al contrario invece di quanto succede utilizzando DOM, in cui prima di poter operare su qualsiasi elemento dobbiamo prima caricare l'interno albero XML. Per manipolare un singolo elemento, ci basta classe XElement
, senza dover per forza definire il documento XML cui questo elemento appartiene.
Vediamo, per esempio, come cambia la selezione di informazioni su un documento XML (in questo caso un feed RSS) con LINQ to XML.
Selezionare tutti gli elementi di un RSS (C#)
XDocument rss = XDocument.Load("http://www.peppedotnet.it/Blog/Rss.aspx"); var posts = from b in rss.Descendants("item") select new { Title = b.Element("title").Value, Link = b.Element("link").Value, PubDate = Convert.ToDateTime(b.Element("pubDate").Value) }; foreach (var post in posts) { Console.WriteLine(post.Title); Console.WriteLine("[{1}] {0}", post.Link, post.PubDate); }
Selezionare tutti gli elementi di un RSS (VB.NET)
Dim rss As XDocument = XDocument.Load("http://www.peppedotnet.it/Blog/Rss.aspx") Dim posts = From b In rss.Descendants("item") _ Select Title = b.Element("title").Value, _ Link = b.Element("link").Value, _ PubDate = Convert.ToDateTime(b.Element("pubDate").Value) For Each post In posts Console.WriteLine(post.Title) Console.WriteLine("[{1}] {0}", post.Link, post.PubDate) Next
L'accesso alle informazioni è diventato molto semplice; per la lettura di un feed RSS in pratica, abbiamo dovuto scrivere solamente 2 righe di codice!
Tramite una query LINQ abbiamo selezionato tutti gli elementi <item>
del feed scelto; sfruttando i tipi anonimi possiamo gestire i dati ritornati dalla query di selezione attraverso una collezione di oggetti tipizzati (la variabile post
infatti, ha le proprietà Title
, Link
e PubDate
fornite dal tipo anonimo creato in fase di selezione).
Anche in questo caso, non dobbiamo limitarci a vedere LINQ to XML solamente come un linguaggio per la semplice selezione di informazioni da documenti XML. Le possibili operazioni sono limitate solamente dalla fantasia.
LINQ to DataSet
Questa terza implementazione offre un linguaggio di semplice selezione su strutture salvate in memoria, che rappresentano delle informazioni secondo il modello relazionale, quindi DataSet
, DataTable
e DataRow
utilizzando i nuovi operatori propri della sintassi LINQ (in questo caso specifico, la creazione di nuove strutture non è stata prevista).
Considerando che, effettivamente, la selezione viene fatta su oggetti di tipo DataTable
, l'unico passo che dobbiamo compiere prima di scrivere le nostre query secondo la sintassi LINQ è quello di trasformare il tipo di questi oggetti in un tipo che vada "a braccetto" con LINQ (per esempio l'interfaccia IEnumerable
). Questa operazione si occupa di portarla a termine il metodo AsEnumerable()
, inserito con la tecnica degli extension methods proprio all'interno della classe DataTable
. Fatto questo, siamo in grado di eseguire le nostre query sulla struttura dati in memoria, con una sintassi molto simile al linguaggio SQL:
(C#)
var query = from r in ds.Tables[0].AsEnumerable()
where r.Field<DateTime>("CampoData").Year == 2008
select r.Field<string>("CampoSelezionato");
(VB.NET)
Dim query = From r In ds.Tables(0).AsEnumerable() _ Where r.Field(Of DateTime)("CampoData").Year = 2008 _ Select r.Field(Of String)("CampoSelezionato")
Nella query d'esempio abbiamo selezionato un singolo campo da una tabella presente in un DataSet
(ds
) ponendo la condizione che il campo CampoData
, di tipo DateTime
, essere dell'anno 2008.
Utilissimo risulta quindi il metodo generico Field()
che permette di evitare errori di null reference a fronte di casting su tipi specifici.
Per poter selezionare invece più colonne nella nostra query, dobbiamo sempre rifarci all'utilizzo della tecnica dei tipi anonimi:
(C#)
var query2 = from r in ds.Tables[0].AsEnumerable() where r.Field<DateTime>("Data").Year == 2008 select new { Titolo = r.Field<string>("Titolo"), Data = r.Field<DateTime>("Data") };
(VB.NET)
Dim query2 = From r In ds.Tables(0).AsEnumerable() _ Where r.Field(Of DateTime)("Data").Year = 2008 _ Select Titolo = r.Field(Of String)("Titolo"), _ Data = r.Field(Of DateTime)("Data")
Inoltre, LINQ to DataSet supporta selezioni sia su normali DataSet
(come abbiamo appena visto) che su DataSet
tipizzati, in cui risulta sicuramente molto più semplice la scelta dei vari campi della tabella e in cui non è necessaria la chiamata al metodo AsEnumerable()
, in quando ogni DataSet
tipizzato deriva già di per sé dall'interfaccia IEnumerable
.
(C#)
CustomersTableAdapter adapter = new CustomersTableAdapter(); TypedDataSet.CustomersDataTable customers = adapter.GetData(); var result = from r in customers where r.IsCityNull() == false && r.City == "London" orderby r.ContactName select r;
(VB.NET)
Dim adapter As New CustomersTableAdapter() Dim customers As TypedDataSet.CustomersDataTable = adapter.GetData() Dim result = From r In customers _ Where r.IsCityNull() = False And r.City = "London" _ Order By r.ContactName _ Select r
In poche parole, se avete fatto bene caso agli esempi appena visti, questa implementazione di LINQ non diverge molto da LINQ to Objects; cambia semplicemente il fatto che le fonti di dati prese in considerazione per le nostre query sono degli oggetti in memoria di tipo DataTable
.
LINQ to SQL
LINQ to SQL risulta sicuramente l'implementazione più importante di LINQ. Ci permette di scrivere query di selezione, inserimento, modifica o cancellazione su dati prelevati da un database SQL Server, integrate direttamente nel codice C# o VB.NET. Tutto questo basandosi su un modello ad oggetti che rispecchia perfettamente (con una forma di mappatura uno a uno) le strutture presenti all'interno della base di dati.
Per essere ancora più chiari LINQ to SQL è un ORM (Object Relational Mapping) che permette di modellare la struttura del proprio database relazione attraverso classi .NET; tale ORM può essere poi interrogato attraverso la sintassi LINQ per permettere all'utente di eseguire query "CRUD" sulle informazioni presenti all'interno del database. Ad oggi, sono però solamente supportati database SQL Server.
Il lavoro che fa praticamente questo ORM è quello di tradurre le query integrate al linguaggio scelto (quindi query LINQ) in query SQL per l'esecuzione sulla base di dati e, in seguito, di tradurre il risultato della query in strutture dati tabulari costituenti delle istanze di oggetti vere e proprie. Una query LINQ di questo tipo quindi:
(C#)
ORMDataContext context = new ORMDataContext(); var customers = from c in context.Customers where c.City == "London" orderby c.ContactName select c;
(VB.NET)
Dim context As New ORMDataContext() Dim result = From c In context.Customers _ Where c.City = "London" _ Select c
verrà poi eseguita all'interno del database secondo questa sintassi SQL:
SELECT [CustomerID],[CompanyName],[ContactName],[ContactTitle],[Address], [City],[Region],[PostalCode],[Country],[Phone],[Fax] FROM [Northwind].[dbo].[Customers] WHERE [City] = 'London'
aumentando notevolmente la facilità di scrittura del codice per l'accesso ai dati relazionali all'interno delle proprie applicazioni (considerando che non ci si deve più preoccupare di creare una connessione al database, lanciare un comando, etc.).
Se invece, vogliamo inserire un nuovo record all'interno del database, non dobbiamo fare altro che creare l'informazione da inserire utilizzando le strutture definite dal mapping LINQ to SQL e sottomettere il nuovo dato all'oggetto che si occupa di gestire le comunicazioni tra l'applicazione e il database.
(C#)
ORMDataContext context = new ORMDataContext(); Customer c = new Customer(); c.CustomerID = "PEPPE"; c.City = "Pesaro"; c.ContactName = "Giuseppe Marchi"; c.Address = "Via col vento 1"; c.CompanyName = "HTML.it"; context.Customers.InsertOnSubmit(c); context.SubmitChanges();
(VB.NET)
Dim context As New ORMDataContext() Dim c As New Customer c.CustomerID = "PEPPE" c.City = "Pesaro" c.ContactName = "Giuseppe Marchi" c.Address = "Via col vento 1" c.CompanyName = "HTML.it" context.Customers.InsertOnSubmit(c) context.SubmitChanges()
Gli esempi di aggiornamento del database e di cancellazione di record sono stati omessi, ma comunque presenti all'interno del codice da scaricare; sicuramente non vi risulterà difficile immaginare che anche in questi ultimi due casi, il modo in cui vengono effettuate operazioni sulla base di dati tramite LINQ to SQL sarà sempre di una semplicità disarmante.
Le potenzialità di LINQ to SQL risultano essere veramente tante e soprattutto, ormai, risulta la scelta ottimale basarsi su di un ORM per gestire l'accesso ai dati e la loro persistenza, all'interno delle proprie applicazioni.