Ultimamente ho avuto modo di riflettere insieme a dei miei colleghi circa gli aspetti in cui LINQ to DataSet interviene per facilitare sensibilmente le interrogazioni in-memory sui DataSet, dando quindi una grossa mano agli sviluppatori di tutte quelle applicazioni .NET data-centric che necessitano di forti meccanismi di caching di dati relazionali. Infatti, fino a ieri il linguaggio bulit-in di interrogazione dei DataSet si rivelava alcune volte poco espressivo o addirittura inadeguato al punto da spingere molti team (ne so qualcosa ;)) ad implementare delle personalizzazioni dei meccanismi di query per ottenere una maggiore produttività soprattutto per quanto concerne operazioni di confronto su collezioni di DataRow.
Grazie a LINQ To DataSet tutti questi sforzi non sono più necessari... anzi, ci viene fornita un'infrastruttura di interrogazione che teoricamente è in grado di soddisfare query arbitrariamente complesse: infatti, le espressioni LINQ to DataSet sono tradotte direttamente in IL e non necessitano di una ulteriore traduzione in un linguaggio di query di un'altra sorgente dati (con tutte le limitazioni e gli svantaggi che ne derivano) come nel caso di LINQ to SQL o LINQ to Entities.
Vediamo alcuni concetti fondamentali. Per ovviare al fatto che i buon vecchi DataTable e le relative DataRowCollection NON implementano l'interfaccia IEnumerable, è stato introdotto un extension method AsEnumerable() per il DataTable. Questo metodo funge da wrapper verso un oggetto di tipo EnumerableRowCollection che di fatto è "LINQ-enabled" poiché costituisce un oggetto di tipo IEnumerable<DataRow>. Niente a questo punto ci vieta di individuare un'analogia con LINQ To Objects, con l' ovvia accortezza di distinguere e gestire il fatto che stiamo trattando collezioni di oggetti di tipo DataRow. Partiamo da un semplice esempio:
IEnumerable<DataRow> queryResult = from customerDataRow in dataTable_Customers.Rows.Cast<DataRow>()
where !customerDataRow.IsNull("Country") &&
(string)customerDataRow["Country"] == "Italy"
select customerDataRow;
Osservando questo codice ci accorgiamo subito di tre aspetti:
- Al posto di AsEnumerable() è stato utilizzato l'extension method Cast<DataRow>() che, analogamente, permette di trasformare la proprietà Rows (di tipo DataRowCollection) del DataTable in un IEnumerable<DataRow>.
- L'accesso al valore di una colonna è "weakly-typed": infatti, tramite la sintassi <DataRow>["<ColumnName>"] si referenzia un object che necessita obbligatoriamente di un cast esplicito al tipo di dato corrispondente.
- L'accesso a valori potenzialmente nulli tramite unboxing richiede il relativo controllo DataRow.IsNull(...) dal momento che la costante DBNull non è "castabile" ad alcun tipo del CLR. Eventuali tentativi di unboxing di valori DBNull genererebbero quindi un exception a meno che non si utilizzano conversioni di tipo Convert.ToXXX(...) che prevedono comunque un valore di ritorno non nullo (es. in C# Convert.ToString(customerDataRow["Country"]) applicato all'esempio precedente avrebbe costituito un' operazione sicura)
E' evidente che la tecnica di interrogazione sopra esposta non è proprio il massimo in quanto può essere facilmente soggetta ad errori di unboxing. Per questo è stato introdotto l'extension method Field<T> per la DataRow, che semplicemente fornisce l'accesso alle colonne come se contenessero tipi 'nullable'. Ecco dunque spiegato perché, in generale, è sempre consigliabile effettuare accesso ai valori di una DataRow tramite l'extension method Field<T>.
IEnumerable<DataRow> queryResult = from customerDataRow in dataTable_Customers.AsEnumerable()
where customerDataRow.Field<string>("Country") == "Italy"
&& customerDataRow.Field<string>("Region") == null
select customerDataRow;
L'ultimo concetto fondamentale che dobbiamo tenere a mente riguarda il comportamento dei "set operators" (Distinct, Union, Intersect, Except) nei confronti di EnumerableRowCollection. Ognugno di questi operatori possiede due overload: il primo (privo di parametri in input) cerca di verificare l'uguaglianza degli elementi delle collection invocando i metodi GetHashCode() e Equals() su ciascuna collection mentre l'altro ammette un IEqualityComparer<T>.
Ma se si vogliono confrontare sorgenti di tipo IEnumerable<DataRow> non possiamo assolutamente fidarci delle reference agli oggetti DataRow in quanto potremmo ottenere risultati non desiderati. Abbiamo quindi bisogno di confrontare almeno i valori contenuti nelle DataRow. A tale scopo è stata introdotta la classe statica DataRowComparer che include al suo interno un meccanismo di default per controllare l'uguaglianza di due DataRow ESCLUSIVAMENTE in base ai valori ed ai tipi (non i nomi!!!) previsti per ciascuna colonna.
Ecco dunque un banale esempio di codice che mostra come effettuare correttamente l'intersezione di due collezioni di DataRow ottenute da due filtri diversi applicati allo stesso DataTable:
IEnumerable<DataRow> query1 = from customerDataRow in dataTable_Customers.AsEnumerable()
where customerDataRow.Field<string>("Country") == "Italy"
&& customerDataRow.Field<string>("City") == "Torino"
select customerDataRow;
IEnumerable<DataRow> query2 = from customerDataRow in dataTable_Customers.AsEnumerable()
where customerDataRow.Field<string>("CompanyName").ToLower().EndsWith("s.p.a.")
select customerDataRow;
// Possiamo eventualmente trasformare i risultati in DataTable
// DataTable customers1 = query1.CopyToDataTable();
// DataTable customers2 = query2.CopyToDataTable();
IEnumerable<DataRow> customers = query1.Intersect(query2, DataRowComparer.Default);
foreach (DataRow dataRow in customers) Console.WriteLine("Cutomer: {0} ", dataRow["ContactName"].ToString());
Technorati tags: LINQ, DataSet