Chapter 18 UsingLINQtoDataSet After completing this chapter, you will be able to: Prepare a DataTable instance so that it uses the IEnumerable interface Treat ADO.NET table values as first-class members of a LINQ query Cast type-neutral column values as strongly typed query values LINQ processes data from a variety of sources, but those sources must first be expressed in a form that LINQ can use. For instance, LINQ expects that all incoming data be stored in a col- lection, one that conforms to either the IEnumerable(Of T) or the IQueryable(Of T) interface. The LINQtoDataSet provider endows ordinary ADO.NET DataTable objects with the ability to participate fully in LINQ queries. It does this by adding the necessary LINQ requirements to relevant ADO.NET classes. This chapter introduces these enhancements and shows you how to employ them to extract data from data sets using the power of LINQ. Understanding the LINQtoDataSet Provider ADO.NET’s DataTable class, as a logical collection of data-laden objects, is the perfect can- didate for inclusion in LINQ queries. Unfortunately, it exhibits two aspects that make it less than useful with LINQ: (1) it implements neither IEnumerable(Of T) nor IQueryable(Of T), and (2) the data values contained in each DataRow instance exist as System.Object instances and only indirectly express their true types through DataColumn definitions. To overcome these deficiencies, the LINQtoDataSet provider adds new extension methods to both the DataTable and DataRow classes. These new features appear in the System.Data. DataSetExtensions assembly (found in the System.Data.DataSetExtensions.dll library file). The assembly defines two classes, DataTableExtensions and DataRowExtensions, that include new extension methods for the DataTable and DataRow classes, respectively. For data tables, there is a new AsQueryable method that acts as the gateway for bringing ADO.NET data into a LINQ query. Dwonloaded from: iDATA.ws 306 On the DataRow side, a new Field(Of T) method moves each data column value from a ge- neric, semi-typeless existence to a strongly typed presence within your queries. It is still up to you, as the programmer, to correctly indicate the type of each field as you add them to the LINQ query syntax. But once defined, you can apply all the standard operators to those fields, including them in projections, filters, and other types of expressions. Note You can use DataRow values within LINQ queries without applying the Field(Of T) exten- sion method. However, these fields will still pose as System.Object instances. This might prevent you from carrying out certain types of query actions on specific fields. Also, you must still resolve the data type of each field before using it in post-query processing. LINQtoDataSet also lets you craft queries that use ADO.NET Typed Data Sets; however, the Entity Framework supercedes most of the advantages of typed data sets. Therefore, LINQ queries against typed data sets are not discussed in this book. Writing Queries with LINQtoDataSet With the exception of the new enumerated methods specific toLINQto DataSet, using ADO.NET DataTable objects in LINQ queries is identical tousing standard collection objects. The first step involves converting a data table to its enumerable equivalent using the and, which can be applied to any DataTable instance. C# // ----- Customer is an existing DataTable instance. var results = from cu in Customer.AsEnumerable() select cu; Visual Basic ' ----- Customer is an existing DataTable instance. Dim results = From cu In Customer.AsEnumerable() Select cu Although the LINQtoDataSet provider includes “DataSet” in its name, the focus in LINQ queries is on the DataTable class. LINQtoDataSet does not consider a DataTable instance’s presence in an overall DataSetto be significant, nor does it examine any DataRelationship objects when processing queries that contain multiple DataTable instances. You must link tables together using LINQ’s standard Join operator or use the Where clause to create an implicit join. Dwonloaded from: iDATA.ws Chapter 18 UsingLINQtoDataSet 307 C# // ----- Explicit join. var results = from cu in Customer.AsEnumerable() join ord in Order.AsEnumerable() on cu.ID equals ord.CustomerID select . // ----- Implicit join var results = from cu in Customer.AsEnumerable() from ord in Order.AsEnumerable() where cu.ID == ord.CustomerID select . Visual Basic ' ----- Explicit join. Dim results = From cu In Customer.AsEnumerable() Join ord In Order.AsEnumerable() On cu.ID Equals ord.CustomerID Select . ' ----- Implicit join Dim results = From cu In Customer.AsEnumerable(), ord In Order.AsEnumerable() Where cu.ID = ord.CustomerID Select . After making the tables part of the query, you can access each row’s individual column values as if they were typical LINQ query properties. As mentioned previously, LINQ will not auto- matically ascertain the data type of any given column; you must explicitly cast each field to its proper type. To cast a field, add the Field extension method to the end of the range variable (the range variables in the previous code sample are cu and ord). Because the implementation of Field uses generics, you must also attach a type name using the language-appropriate syntax. Pass the name of the column as an argument to Field. C# var results = from cu in Customer.AsEnumerable() orderby cu.Field<string>("FullName") select new { CustomerName = cu.Field<string>("FullName") }; Visual Basic Dim results = From cu In Customer.AsEnumerable() Select CustomerName = cu.Field(Of String)("FullName") Order By CustomerName Dwonloaded from: iDATA.ws 308 Microsoft ADO.NET 4 Step by Step The Field method includes a few overloaded variations. In addition to field names, you can use a zero-based column position to locate field data, although this might reduce readability in your queries. An additional argument lets you specify the DataRowVersion to use. By de- fault, queries use the DataRowVersion.Current version of the row. Even when enumerated DataTable objects play a key role in a LINQ query, they need not be the only source of data involved. Part of LINQ’s appeal is that it allows you to write queries that involve data from disparate sources. You can mix LINQ to Objects and LINQto DataSet content in the same query simply by including each source in the From clause. C# // ----- Build an ad hoc collection, although you could also // include a fully realized class. var statusTable[] = { new { Code = "P", Description = "Active Order" }, new { Code = "C", Description = "Completed / Shipped" }, new { Code = "X", Description = "Canceled" }}; // ----- Link ADO.NET and Object collections in one query. var results = from ord in Order.AsEnumerable() join sts in statusTable on ord.Field<string>("StatusCode") equals sts.Code orderby ord.Field<long>("ID") select new { OrderID = ord.Field<long>("ID"), CurrentStatus = sts.Description }; Visual Basic ' ----- Build an ad hoc collection, although you could also ' include a fully realized class. Dim statusTable = {New With {.Code = "P", .Description = "Active Order"}, New With {.Code = "C", .Description = "Completed / Shipped"}, New With {.Code = "X", .Description = "Canceled"}} ' ----- Link ADO.NET and Object collections in one query. Dim results = From ord In Order.AsEnumerable() Join sts In statusTable On _ ord.Field(Of String)("StatusCode") Equals sts.Code Select OrderID = ord.Field(Of Long)("ID"), CurrentStatus = sts.Description Order By OrderID As in LINQto Objects, the actual processing of a LINQtoDataSet query does not occur until your code references content from a constructed query. However, all the involved DataTable instances must already be filled with valid data before you make the query. When dealing Dwonloaded from: iDATA.ws Chapter 18 UsingLINQtoDataSet 309 with data from external sources, you must bring any data you plan to include in a LINQ query into the relevant DataTable instances before passing the objects through LINQ. If you use a DataAdapter to load data, call its Fill method before usingLINQto extract data. Note The DataAdapter object’s Fill method loads all requested data into local DataSet memory. If the tables you need to query with LINQ are large and you aren’t able to first reduce the num- ber of ADO.NET-managed rows, you might wish to consider alternatives toLINQto DataSet. LINQto Entities, discussed in Chapter 19, “Using LINQto Entities,” can process external data without the need to load full tables into memory. Querying with LINQto DataSet: C# 1. Open the “Chapter 18 CSharp” project from the installed samples folder. The project includes a Windows.Forms class named OrderViewer, which is a simple order-list viewer. 2. Open the source code view for the OrderViewer form. Locate the GetConnectionString function; this is a routine that uses a SqlConnectionStringBuilder to create a valid con- nection string to the sample database. It currently includes the following statements: sqlPortion.DataSource = @"(local)\SQLExpress"; sqlPortion.InitialCatalog = "StepSample"; sqlPortion.IntegratedSecurity = true; Adjust these statements as needed to provide access to your own test database. 3. Locate the ActView_Click event handler. This routine displays a list of orders for either all customers in the database or for a specific customer by ID number. Just after the “Retrieve all customer orders” comment, add the following statement: var result = from cu in customerTable.AsEnumerable() from ord in orderTable.AsEnumerable() from sts in statusTable where cu.Field<long>("ID") == ord.Field<long>("Customer") && ord.Field<string>("StatusCode") == sts.Code orderby cu.Field<string>("FullName"), ord.Field<long>("ID") select new { CustomerID = cu.Field<long>("ID"), CustomerName = cu.Field<string>("FullName"), OrderID = ord.Field<long>("ID"), OrderStatus = sts.Description, OrderDate = ord.Field<Date>("OrderDate"), OrderTotal = ord.Field<decimal>("Total") }; This query combines two DataTable instances (customerTable and orderTable, each decorated with the AsEnumerable extension method) with a collection of local object instances (statusTable). It forms implicit inner joins between the tables via the where clause and performs a projection of fields from each source table. Dwonloaded from: iDATA.ws 310 Microsoft ADO.NET 4 Step by Step 4. Just after the “Filter and display the orders” comment, add the following lines: var result2 = result.Where(ord => ord.CustomerID == long.Parse(CustomerID.Text)); AllOrders.DataSource = result2.ToList(); These statements filter the original query by selecting those records that include a user- specified customer ID. This segment uses LINQ extension methods and a lambda ex- pression, which works well with the LINQtoDataSet provider. The second line displays the results. 5. Just after the “Just display the original full results” comment, add the following statement: AllOrders.DataSource = result.ToList(); This code displays the query results when no further customer ID filtering is needed. 6. Run the program. To see orders, select the Include One Customer By ID option, enter 1 in the Customer ID field, and then click View. The grid displays content from each of the three source tables. For example, the CustomerName column shows a value from the ADO.NET Customer table, the OrderDate column comes from the Order table, and OrderStatus gets its information from the ad hoc in-memory statusTable collection. Querying with LINQto DataSet: Visual Basic 1. Open the “Chapter 18 VB” project from the installed samples folder. The project in- cludes a Windows.Forms class named OrderViewer, which is a simple order-list viewer. Dwonloaded from: iDATA.ws Chapter 18 UsingLINQtoDataSet 311 2. Open the source code view for the OrderViewer form. Locate the GetConnectionString function; this is a routine that uses a SqlConnectionStringBuilder to create a valid con- nection string to the sample database. It currently includes the following statements: sqlPortion.DataSource = "(local)\SQLExpress" sqlPortion.InitialCatalog = "StepSample" sqlPortion.IntegratedSecurity = True Adjust these statements as needed to provide access to your own test database. 3. Locate the ActView_Click event handler. This routine displays a list of orders for either all customers in the database or for a specific customer by ID number. Just after the “Retrieve all customer orders” comment, add the following statement: Dim result = From cu In customerTable.AsEnumerable(), ord In orderTable.AsEnumerable(), sts In statusTable Where cu.Field(Of Long)("ID") = ord.Field(Of Long)("Customer") _ And ord.Field(Of String)("StatusCode") = sts.Code Select CustomerID = cu.Field(Of Long)("ID"), CustomerName = cu.Field(Of String)("FullName"), OrderID = ord.Field(Of Long)("ID"), OrderStatus = sts.Description, OrderDate = ord.Field(Of Date)("OrderDate"), OrderTotal = ord.Field(Of Decimal)("Total") Order By CustomerName, OrderID This query combines two DataTable instances (customerTable and orderTable, each decorated with the AsEnumerable extension method) with a collection of local object instances (statusTable). It forms implicit inner joins between the tables via the Where clause and performs a projection of fields from each source table. 4. Just after the “Filter and display the orders” comment, add the following lines: Dim result2 = result.Where(Function(ord) ord.CustomerID = CLng(CustomerID.Text)) AllOrders.DataSource = result2.ToList() These statements filter the original query by selecting those records that include a user- specified customer ID. This segment uses LINQ extension methods and a lambda ex- pression, which works well with the LINQtoDataSet provider. The second line displays the results. 5. Just after the “Just display the original full results” comment, add the following statement: AllOrders.DataSource = result.ToList() This code displays the query results when no further customer ID filtering is needed. Dwonloaded from: iDATA.ws 312 Microsoft ADO.NET 4 Step by Step 6. Run the program. To see orders, select the Include All Customers option and then click View. The grid displays content from each of the three source tables. For example, the CustomerName column shows a value from the ADO.NET Customer table, the OrderDate column comes from the Order table, and OrderStatus gets its information from the ad hoc in-memory statusTable collection. Summary This chapter introduced LINQto DataSet, an ADO.NET-focused variation of LINQto Objects. The implementation of the LINQtoDataset provider shares a close relationship and syntax with the base LINQto Objects implementation. By applying a few simple extension methods, DataTable objects can become part of independent or integrated data queries. Beyond LINQto Objects, LINQtoDataSet is probably the easiest of the LINQ providers to use in your application. Its only drawback is that it expects any queried data to be memory- resident, something not required by other LINQ providers that extract content from external databases. Dwonloaded from: iDATA.ws Chapter 18 UsingLINQtoDataSet 313 Chapter 18 Quick Reference To Do This Include a DataTable instance in a LINQ query Call the DataTable object’s AsEnumerable extension method. Pass the results of this call as a table source in the query. Include a DataTable column in a LINQ query Add the DataTable as a source within the query, adding a range variable. Call the range variable’s Field extension method, indicat- ing the generic data type and the name of the column. Dwonloaded from: iDATA.ws Dwonloaded from: iDATA.ws . ADO.NET-managed rows, you might wish to consider alternatives to LINQ to DataSet. LINQ to Entities, discussed in Chapter 19, Using LINQ to Entities,” can process. tables together using LINQ s standard Join operator or use the Where clause to create an implicit join. Dwonloaded from: iDATA.ws Chapter 18 Using LINQ to DataSet