In part 1 of this 2 part series I provided you with a basic introduction to Language Integrated Query, or LINQ. In this second part I take a look at several specific implementations of LINQ, including LINQ to Objects, LINQ to DataSets, and LINQ to XML. I also have an opportunity to introduce iterator methods, special methods that you implement to return a sequence for use by LINQ queries.
LINQ to Objects
LINQ to Objects is any version of LINQ that does not employ a LINQ provider. Consequently, all LINQ queries shown in part 1 of this series are examples of LINQ to Objects.
Here is another example.
var Customers := GetCustomers;
var query := from c in Customers
where (c.Age > 10)
and (c.Age < 40)
and (c.Active = true<) select c;
for each m in query do
ResultsList.Items.Add(
String.Format("Name: {0}, Age={1}",
[m.Name, m.Age]));
Note: If you had wanted to create a query that produced a sequence of objects that only included the Name and Age members, you could have used the technique shown in part 1 where the select clause returned a newly created object (which was an anonymous object in that example, but could have been any compatible type).
In the preceding query, a sequence of Customer objects is returned by the GetCustomers function. This particular function is an example of an iterator method. Iterator methods are discussed in the following section.
Granted, this is a simple example. However, there are many different methods in the .NET framework class library that return arrays, collections, and other queryable data sources. As a result, it is possible to use LINQ to perform a wide variety of useful tasks.
Examples of some of these uses can be found in the Windows SDK for .NET 3.5. For example, there are a number of examples of how to use LINQ to explore files and directories on the local file system. This is possible because the System.IO.Directory.GetFiles method returns an array of strings containing the names of the files in a given directory. Arrays, as you’ve seen, can be used in LINQ queries.
Interator Methods and Yield Statements
Iterators are special methods that return a sequence. You declare an iterator by including the iterator keyword in the method signature. Each element in the sequence is specifically returned by the iterator through a call to yield.
This is demonstrated in the following code sample. First, the iterator is declared using the following statement.
method GetLowDigits: sequence of Integer; iterator;
Next, the implementation of the iterator includes one or more calls to yield, which, like exit, can be used to return a value (though yield does not cause a return from the method in the same way that exit does).
method MainForm.GetLowDigits: sequenbce of Integer;begin
for i: Integer := 1 to 9 do
yield i;
end;
The following code demonstrates the use of this iterator.
var numbers := GetLowDigits;for each n in numbers do
MessageBox.Show(n.ToString);
Iterator methods are not executed when the sequence is assigned to a sequence reference. Specifically, the var declaration and initialization in the preceding code segment did not cause the GetLowDigits iterator to execute. Instead, it was the specific iteration over the sequence in the for each loop that caused the iterator to be executed. Furthermore, the iterator was not executed all at once. Instead, the iterator is executed up until it returns the first element of the sequence, that element is then used by the for each loop, which will then call back to the iterator to get the next value in the sequence, and so on.
The yield keyword can be used to return either a single element of the sequence or a reference to a sequence (which could be an array, a method that returns an array, or other similar reference).
Here is another example of an iterator and a sequence. This code requires a type declaration, which looks like the following.
Customer = classprivate
FName: String;
FAge: Integer;
public
property Name: String read FName write FName;
property Age: Integer read fAge write FAge;
end;
Here is the declaration of the iterator.
method GetCustomers: sequence of Customer; iterator;
And here is the implementation of the iterator.
method MainForm.GetCustomers: sequence of Customer;begin
yield new Customer(Name:= 'Allan', Age := 10);
yield new Customer(Name := 'Bob', Age := 25);
yield new Customer(Name := 'Craig', Age := 18);
end;
Now that you understand the iterator, it is easy to see that the following LINQ query, which was introduced in the preceding section, populates the listbox named ResultList with the values Name: Bob, Age=25 and Name: Craig, Age=18.
var Customers := GetCustomers;
var query := from c in Customers
where (c.Age > 10) and (c.Age < 40) and (c.Active = true<) select c;
for each m in query do
ResultsList.Items.Add(
String.Format("Name: {0}, Age={1}", [m.Name, m.Age]));
The implementation of the iterator in this case used the named parameters feature of Delphi Prism’s nameless constructors to return each of the customers (which in reality would have probably been populated with data from an external source, such as a database). Also, the Customers type declaration used type inference to determine that Customers variable was a sequence of Customer.
What is LINQ to DataSets
LINQ to DataSets is a LINQ provider that permits you to use LINQ queries against DataTables, DataRows, and other classes in the ADO.NET framework. In order to use LINQ to DataSet, you must add the System.Data.DataSetExtensions assembly to the references section of your Delphi Prism project.
This section is designed to provide you with a brief introduction to LINQ to DataSet. As such, it does not go into ADO.NET, which is the data connectivity framework in .NET.
Note: LINQ to DataSet should really probably be called LINQ to DataTable or LINQ to DataRows, or even LINQ to ADO.NET. None of the operations in LINQ to DataSet are performed on instances of the DataSet class. Instead, they are performed on DataTables and DataRows.
As you learned earlier in this article, LINQ queries can be performed on any object that implements the IEnumerable<T> interface. DataTables (and DataRows) do not implement IEnumerable. However, they both have extension methods that support IEnumerable. These are defined in the DataTableExtensions class, which is located in the System.Data.DataSetExtensions namespace (which is why you must add this assembly to your project references folder).
There are only three extension methods in the DataSetExtensions class. These are AsDataView, AsEnumerable, and CopyToDataTable. For DataTables, the key method is AsEnumerable, which returns an enumerable collection of DataRows (records).
This is demonstrated in the following segment code.
method MainForm.LINQToDataSet_Click(sender: System.Object;
e: System.EventArgs);
var
Connection: DataStoreConnection;
Adapter: DataStoreDataAdapter;
DataTable1: DataTable;
begin
Connection := new DataStoreConnection(conStr);
Connection.Open;
try
Adapter := new DataStoreDataAdapter(
'SELECT * FROM CUSTOMER', connection);
DataTable1 := new DataTable;
DataTable1.TableName := 'Customer';
Adapter.Fill(DataTable1);
var query :=
from cust in DataTable1.AsEnumerable
where cust['ON_HOLD'].Equals(DbNull.Value)
and (cust.Field<string>('STATE_PROVINCE') = 'CA');
for each c in query do
begin
ResultsList.Items.Add(
String.Format('Customer Number: {0}' +
' Company Name: {1}', c['Cust_No'].ToString,
c['Customer'].ToString));
end;
finally
Connection.Close;
end;
end;
Once the DataTable has been populated with data from the SQL query, the AsEnumerable method of the DataTable is called, and this object is used in the LINQ query. Interestingly enough, the object returned from the AsEnumerable method is a generic IEnumerable<T>, where the type is a collection of DataRows.
As you consider this code segment, you might conclude that you would be better off using a SQL statement that performs the filtering of the results, rather than use a LINQ query. While that may be true in some instances, if you want to load a DataTable once, and perform numerous queries on the returned value, the LINQ query offers performance benefits. (Note that another alternative is to use a DataView to filter and sort the DataTable results.)
Note that in addition to LINQ to DataSet, the .NET framework provides the LINQ to SQL provider. Unlike LINQ to DataSet, which can be used against any database for which there is a .NET data provider, LINQ to SQL is specifically designed to work only with Microsoft SQL Server. LINQ to Entities is another LINQ provider that can be used against supported databases.
What is LINQ to XML
LINQ to XML provides a programming model, similar to XPath, with which you can create, edit, query, and save XML documents. LINQ to XML provides you an alternative to using the XML DOM (document object model), which is what is used by the XMLDocument class in the .NET framework. Like the DOM, LINQ to XML works with the XML document in memory. From there you can read, query, and write data to the XML document, after which you can stream it to a service or write it to a file, if you desire.
As is the case with the LINQ to DataSet topic that precedes this section, this discussion is designed to provide you with a basic introduction to LINQ to XML. It does not, however, cover general XML issues.
In fact, LINQ to XML is far more involved than LINQ to DataSet. This is because LINQ to XML not only supports features necessary for querying, but as mentioned earlier, an entire programming model for working with XML. As a result, this section will cover just enough information to get you started.
Unlike LINQ to DataSet, where most of the functionality is found in extension methods added to DataTable and DataRow, LINQ to XML, found in the System.Xml.Linq namespace, includes a large number of concrete classes that you can use to work with XML documents.
Of these classes, there are three that you are likely to use most often. These are XDocument and XElement, which descend from XNode, and XAttribute, which descends from XObject. (XNode, by the way, also descends from XObject.)
You can create a valid XML document using either XDocument or XElement, though XDocument provides a little more support for this. Specifically, there are certain advanced features of XML documents that you can access through the XDocument class which are unavailable through the XElement class. XAttribute, by comparison, is used to define attributes of XML elements.
But before you get started, at a minimum you will need to add the System.Xml.Linq namespace to your uses clause. In addition, you may have to add one or more of the following additional namespaces, depending on what features your code will use: System.Xml, System.Xml.Schema, System.Xml.XPath, and System.Xml.Xsl.
Creating XML Documents using LINQ to XML
As mentioned previously, you can create XML documents using either the XDocument or the XElement class. The following example demonstrates how to create an XML document using the XElement class.
element := new XElement('customers',
new XElement('customer',
new XAttribute('custno', 1001),
new XElement('name', 'John Doe'),
new XElement('address',
'101 Broadway Avenue'),
new XElement('city', 'New York'),
new XElement('state', 'NY'),
new XElement('zip',
'00123') ),
new XElement('customer',
new XAttribute('custno', 1002),
new XElement('name', 'John Doe'),
new XElement('address',
'1001 Main Street'),
new XElement('city', 'Los Angeles'),
new XElement('state', 'CA'),
new XElement('zip', '90123')
)
);
element.Save(XmlFileName);
This code creates an XML file with a root element named customers. The root element has two child elements named customer. Each child element has one attribute and four child elements. The following is how the XML file created by this code looks.
<?xml version="1.0" encoding="utf-8"?><customers>
<customer custno="1001">
<name>John Doe</name>
<address>101 Broadway Avenue</address>
<city>New York</city>
<state>NY</state>
<zip>00123</zip>
</customer>
<customer custno="1002">
<name>John Doe</name>
<address>1001 Main Street</address>
<city>Los Angeles</city>
<state>CA</state>
<zip>90123</zip>
</customer>
</customers>
Creating this same file using an XDocument looks similar, though there are differences.
The documentation for LINQ with XML includes a large number of demonstrations of XML definition using declarations like the one provided previously. These declarations, however, are anything but flexible. In other words, they always create the same XML file, which is rarely useful.
The following example, which obtains its data from an ADO.NET data reader, produces an XML file similar in structure to the preceding one, except that its data is entirely based on the results of a SQL query.
Connection := new DataStoreConnection(conStr);
Connection.Open;
try
Command := Connection.CreateCommand;
Command.CommandText := 'select cust_no, customer, ' +
'address_line1, city, state_province, ' +
'postal_code from customer';
DataReader := Command.ExecuteReader;
try
document := new XDocument(
new XElement('customers'));
while DataReader.Read do
begin
attribute := new XAttribute('custno',
DataReader.GetString(0));
element :=
new XElement('customer',
new XElement('name', DataReader.GetString(1)),
new XElement('address',
DataReader.GetString(2)),
new XElement('city', DataReader.GetString(3)),
new XElement('state',
DataReader.GetString(4)),
new XElement('zip',
DataReader.GetString(5))
);
element.Add(attribute);
document.Root.Add(element);
end;
finally
DataReader.Close;
end;
document.Save(XmlFileName);
finally
connection.Close;
end;
Querying with LINQ to XML
Querying using LINQ to XML is similar to other LINQ queries, though there are classes and methods that you use in LINQ to XML that are not found in other LINQ technologies. In short, once you have a reference to a queryable object (an IEnumerable, a sequence, or other similar construct), you use query statements to retrieve the data you are interested in. The following example demonstrates a LINQ to XML query.
element := XElement.Load(XmlFileName);
var childList :=
from el in element.Elements
where String(el.Element('state')) = 'CA'
order by String(el.Attribute('custno')) desc
select new XElement('document',
new XElement('CustomerName', el.Element('name')) ,
new XElement('Address', el.Element('address')),
new XElement('CityStateZip',
el.Element('city').Value + ', ' +
el.Element('state').Value + ' ' +
el.Element('zip').Value));
for each e in childList do
begin
ResultsList.Items.Add(
e.Element("CustomerName").Value);
ResultsList.Items.Add(e.Element("Address").Value);
ResultsList.Items.Add(
e.Element('CityStateZip').Value);
ResultsList.Items.Add(String.Empty);
end;
This query returns a sequence of XElement references, which have a structure similar to, though different than, the original XElements. For example, the new XElements are not named customer, they are named document. Furthermore, there are no attributes in the new XElements, and there are only three child nodes.
The preceding example demonstrated a query using the XElement class. The following code shows the same basic query, however an XDocument is used in this example.
document := XDocument.Load(XmlFileName);
var childList :=
from el in document.Descendants
where String(el.Element('state')) = 'CA'
order by String(el.Attribute('custno')) desc
select new XElement('document',
new XElement('CustomerName',
el.Element('name')),
new XElement('Address', el.Element('address')),
new XElement('CityStateZip',
el.Element('city').Value + ', ' +
el.Element('state').Value + ', ' +
el.Element('zip').Value));
for each e in childList do
begin
ResultsList.Items.Add(
e.Element("CustomerName").Value);
ResultsList.Items.Add(e.Element("Address").Value);
ResultsList.Items.Add(
e.Element('CityStateZip').Value);
ResultsList.Items.Add(String.Empty);
end;
Modifying Exiting XML using LINQ to XML
Once XML is in memory, you can add elements or attributes, remove elements or attributes, as well as modify the data for elements or attributes. If you load the XML from an existing source, such as a file, make modifications, and then save the XML back to the original source, you have effectively changed the XML.
This is demonstrated in the following code.
document := XDocument.Load(XmlFileName);
var childList :=
from el in document.Descendants
where String(el.Element('state')) = 'CA'
order by String(el.Attribute('custno')) desc
select el;
//make changes
for each e in childList do
begin
e.Element('state').SetValue('California');
end;
//save the changes
document.Save(XmlFileName);
There is an important characteristic of the preceding code that you should note. Specifically, the select clause of the LINQ to XML query returned a sequence of XElements from the original XDocument. While these represent a possible subset of all XElements returned by the Descendants method of the XDocument, they are still child elements of the original XDocument.
When changes are made to the items in the sequence returned by the query, those changes are actually being made to the corresponding XElements in the XDocument. This is why the changes are preserved when the subsequent call to XDocument.Save is made.
By comparison, if the select clause had created new XElements, similar to how the last query in the preceding section demonstrated, the subsequent changes would have been performed on the newly created objects, which are not child elements of the XDocument.
Summary
This two part series has provided you with a brief introduction to LINQ with Delphi Prism. Here you have learned how to construct LINQ Queries, the nature of LINQ to Objects, define and implement iterators, and have been given a preview of LINQ to DataSets and LINQ to XML.
Copyright © 2009 Cary Jensen. All Rights Reserved