Accessing Data with Microsoft .NET Framework 4: LINQ to XML

  • 6/15/2011

Lesson 2: Querying with LINQ to XML

The use of LINQ to XML provides a powerful means to query XML data, using a language-based syntax that supports IntelliSense. LINQ to XML also enables you to transform XML in a very simple manner. This lesson covers the various aspects of querying XML by using LINQ to XML, and the next lesson explores transformations.

Introducing the XDocument Family

In the quest to design a way to use LINQ over XML data, Microsoft needed to create a new set of classes that would simplify the task, so it created the XDocument classes: XElement, XAttribute, XNamespace, XDirective, and more. To use these classes, you must add a reference to the System.Xml.Linq.dll assembly, and then you can add an imports (C# using) System.Xml.Linq statement to your code. Figure 5-1 shows many of the classes that comprise the XDocument family.

As Figure 5-1 shows, many, but not all, classes inherit from the XNode class. In the class hierarchy, the XAttribute class is not an XNode. This lesson focuses on the XDocument, XElement, XAttribute, and XNamespace classes and their parent classes.

Figure 5-1

Figure 5-1 The XDocument class family simplifies LINQ access to XML data.

The XObject Class

Most of the XDocument classes derive from the XObject class. This is a MustInherit (C# abstract) class that contains the following members that will be inherited.

  • BaseUri A read-only property containing the base URI string for the current object.

  • Document A read-only property that references the XDocument object to which the current object belongs.

  • NodeType An abstract, read-only property that returns XmlNodeType of the current node. Child classes override this to return the proper node type.

  • Parent A read-only property that returns the XElement parent of the current node.

  • AddAnnotation A method for adding an annotation object to the object’s annotations list. Think of an annotation as something like a notation—or, better yet, as an attachment. The annotation is not limited to being a string; it can be any object you want to attach to the current XObject instance. Annotations are not persisted and do not show when using the ToString method to retrieve the XML representation as a string.

  • Annotation Generic and non-generic methods that return the first annotation of the type passed to this method as a generic parameter or regular method parameter.

  • Annotations Generic and non-generic methods that return a collection of the type passed to this method as a generic parameter or regular method parameter.

  • RemoveAnnotations Generic and non-generic methods that remove the annotations of the type passed to this method as a generic parameter or regular method parameter.

  • Changed An event that occurs if the current XObject class or any of its descendents have changed.

  • Changing An event that occurs before the current XObject class or any of its descendents change.

In addition to these members, the XObject class explicitly implements the IXmlLineInfo interface, which contains LineNumber and LinePosition properties and the HasLineInfo method.

The XAttribute Class

The XAttribute object represents an XML attribute. The XAttribute class is derived from the XObject class, so it inherits all the members of the XObject class. In addition, the XAttribute class contains the following members.

  • EmptySequence A shared (C# static) read-only property that returns an empty generic IEnumerable of XAttribute.

  • IsNameSpaceDeclaration A read-only property that returns true if the current XAttribute object represents a namespace identifier.

  • Name A read-only property that returns the name of the XML attribute as a string.

  • NextAttribute A read-only property that returns a reference to the next XML attribute. Returns null if the current XAttribute object has no parent or if the current XAttribute object is the last XML attribute of the parent.

  • NodeType Overrides the inherited NodeType property from XObject. Returns XmlNodeType.XAttribute.

  • PreviousAttribute A read-only property that returns a reference to the previous XML attribute. Returns null if the current XAttribute object has no parent or if there is no previous XML attribute.

  • Value A string property that gets or sets the value of the XML attribute. When you attempt to set this property, the property is validated before the value is set. If you attempt to assign Nothing (C# null) to this property, an ArgumentNullException is thrown. Also, if this attribute is a namespace declaration, the namespace is validated. If validation succeeds, the Changing event inherited from XObject is raised, the value is set, and the Changed event is raised.

  • Operator A group of 25 explicit conversion operators that enable you to convert the XAttribute object to many types by using CType (C# explicit cast) based on the XML attribute’s value. Internally, this uses the XmlConvert class to convert the value of the XML attribute to the desired type.

  • Remove A method that removes the current XAttribute object from the parent element. It throws an InvalidOperationException if the current XAttribute object has no parent.

  • SetValue A method that attempts to make the object parameter a string that is passed in and assigns the string to the current XAttribute object’s value.

  • ToString A method that overrides the ToString method inherited from the Object class. This override emits the name and value of the XML attribute as you would expect to see it in an XML document.

The XNode Class

The XNode class is derived from the XObject class, so it contains all the XObject members. The XNode class contains the following members.

  • DocumentOrderComparer A shared (C# static), read-only property that returns a comparer of the type XNodeDocumentOrderComparer. This comparer can compare the relative positions of two nodes.

  • EqualityComparer A shared (C# static), read-only property that returns a comparer of the type XNodeEqualityComparer. This comparer can compare two nodes for equality.

  • NextNode A read-only property that returns a reference to the next node or Nothing (C# null) if there is no next node.

  • PreviousNode A read-only property that returns a reference to the previous node or Nothing (C# null) if there is no previous node.

  • AddAfterSelf A method that enables you to pass in content that should be added after the current node object.

  • AddBeforeSelf A method that enables you to pass in content that should be added before the current node object.

  • Ancestors A method that accepts a name parameter and returns a collection of the ancestor elements of the current node that have the specified name.

  • CompareDocumentOrder A shared (C# static) method that compares two nodes to determine their relative XML document order. This method accepts two XNode objects and returns an Integer (C# int) value by which 0 indicates that the two nodes are equal, -1 indicates that the first node is before the second node, and +1 indicates that the first node is after the second node.

  • CreateReader A method that returns an XmlReader that can read the contents of the current node and its descendents.

  • DeepEquals A shared (C# static) method that accepts two XNode parameters. This method compares the values of the two elements and the values of the descendents and returns true if the nodes are equal.

  • ElementsAfterSelf A method that returns a collection of the sibling element nodes after this node, in document order. There is also an overload that accepts an XName object consisting of the name of the elements you want to return.

  • ElementsBeforeSelf A method that returns a collection of the sibling element nodes before this node, in document order. There is also an overload that accepts an XName object consisting of the name of the elements you want to return.

  • IsAfter A method that accepts an XNode parameter and returns true if the current node is after the XNode passed into this method.

  • IsBefore A method that accepts an XNode parameter and returns true if the current node is before the XNode passed into this method.

  • NodeAfterSelf A method that returns a reference to the node after the current node. Returns Nothing (C# null) if there is no node after the current node.

  • NodeBeforeSelf A method that returns a reference to the node before the current node. Returns Nothing (C# null) if there is no node before the current node.

  • ReadFrom A shared (C# static) method that accepts an XmlReader parameter and returns an XNode object that represents the first node encountered by XmlReader.

  • Remove A method that removes the current node from its parent. This throws an InvalidOperationException if there is no parent.

  • ReplaceWith A method that replaces the node with the specified content passed in as a parameter. The content can be simple content, a collection of content objects, a parameter list of content objects, or null.

  • ToString A method that overrides the ToString method in Object. This returns the XML of the current node and its contents as a string.

  • WriteTo A MustImplement (C# abstract) method that takes an XmlWriter parameter.

The XContainer Class

The XContainer class represents a node that can contain other nodes and is a MustInherit (C# abstract) class that derives from the XNode class. It inherits the members of XNode and XObject. The XDocument and XElement classes derive from this class. The following is a list of its members.

  • FirstNode A read-only property that returns a reference to the first node in this container. If there are no nodes in the container, this property returns Nothing (C# null).

  • LastNode A read-only property that returns a reference to the last node in this container. If there are no nodes in the container, this property returns Nothing (C# null).

  • Add A method that accepts one or several content objects and adds them and their children to this container.

  • AddFirst A method that accepts one or several content objects and adds them and their children to the current XContainer object as the first node.

  • CreateWriter A method that returns an XmlWriter object that can be used to add elements or attributes to the current XContainer object.

  • DescendantNodes A method that returns descendant elements plus leaf nodes contained in the current XContainer object.

  • Descendants A method that returns references to the descendant nodes of the current XContainer object as a generic IEnumerable of XElement. There is also an overload that accepts a name as an XName parameter and returns references only to the descendant nodes that match the name parameter. This method does not return a reference to the current XContainer object.

  • Element A method that accepts a name as an XName parameter and returns a reference to the first child element with a name that matches the name parameter or returns null if no match is found.

  • Elements A method that returns references to all child elements of the current XContainer object as a generic IEnumerable of XElement. There is also an overload that accepts a name as an XName parameter and returns references only to the child nodes that match the name parameter.

  • Nodes A method that returns a reference to all nodes in the current XContainer object as a generic IEnumerable of the XNode object.

  • RemoveNodes A method that removes all nodes from the current XContainer object. Note that this method does not remove any attributes the XContainer object might have.

  • ReplaceNodes A method that accepts one or several content objects as a parameter and replaces the existing content with the content parameter.

The XElement Class

The XElement class represents an XML element with a Name property of the type XName. The XName class is composed of a local name and a namespace. Optionally, XElement can contain XML attributes of type XAttribute, derives from XContainer, and inherits the members of XContainer, XNode, and XObject.

The XElement class explicitly implements the IXmlSerializable interface, which contains the GetSchema, ReadXml, and WriteXml methods so this object can be serialized. The following are members of the XElement class.

  • EmptySequence A shared (C# static), read-only property that returns an empty generic IEnumerable of the XElement object.

  • FirstAttribute A read-only property that returns a reference to the first XML attribute on this element or null if there are no attributes.

  • HasAttributes A read-only property that returns true if the current element has at least one attribute.

  • HasElements A read-only property that returns true if the current element has at least one child element.

  • IsEmpty A read-only property that returns true if the current element has no child elements.

  • LastAttribute A read-only property that returns a reference to the last XML attribute on this element or null if there are no attributes.

  • Name A read–write property that contains the name of the current element as an XName object. The XName object contains a local name and a namespace.

  • NodeType A read-only property that returns XmlNodeType.Element.

  • Value A read–write string property that contains the text of the current element as an XName object. If the current element contains a mixture of text and child elements, the text of all children is concatenated and returned. If you pass an XName into the property setter, all existing content will be removed and replaced with the XName object.

  • AncestorsAndSelf A method that returns a generic IEnumerable of an XElement sequence containing references to the current element and all its ancestor elements. There is also an overload that accepts an XName parameter for the name of the elements you want to retrieve.

  • Attribute A method that accepts a name parameter as an XName and returns a reference to the attribute with a name that matches on the current element. This method returns Nothing (C# null) if no attribute is found.

  • Attributes A method that returns a generic IEnumerable of an XAttribute sequence containing references to all attributes of the current element. There is also an overload that accepts a name parameter as an XName and returns a reference to the attribute with a name that matches on the current element. This method returns an empty sequence if no attribute is found.

  • DescendantNodesAndSelf A method that returns a generic IEnumerable of an XNode sequence containing references to the current node and all its descendant nodes.

  • DescendantsAndSelf A method that returns a generic IEnumerable of an XElement sequence containing references to the current element and all its descendant elements. There is also an overload that accepts a string parameter for the name of the elements you want to retrieve.

  • GetDefaultNamespace A method that returns the default namespace of the current element as an XNamespace object. The XNamespace type has a NamespaceName property that contains the URI of the namespace. If there is no default namespace, this method returns the XNamespace.None value.

  • GetNamespaceOfPrefix A method that accepts a prefix parameter as a string and returns a reference to the associated namespace or null if the namespace is not found.

  • GetPrefixOfNamespace A method that returns the prefix of the current element as a string.

  • Load A shared (C# static) method that creates a new XElement object and initializes it with the contents passed into the method. There are eight overloads for this method that enable you to pass various objects as parameters, such as Stream, TextReader, XmlReader, or a string URI.

  • Operator There are 25 explicit conversion operators, which enable you to convert the XElement object to many types by using CType (C# explicit cast) based on the XML element’s value. Internally, this uses the XmlConvert class to convert the value of the XML element to the desired type.

  • Parse A shared (C# static) method that creates a new XElement object and initializes it with the contents parameter as string passed into the method. There is also an overload for this method that enables you to pass load options to preserve white space.

  • RemoveAll A method that removes all attributes and all elements from the current element.

  • RemoveAttributes A method that removes all attributes from the current element.

  • ReplaceAll A method that replaces the child nodes and attributes of the current element with the content parameter passed into this method. The content can be simple content, a collection of content objects, a parameter list of content objects, or null.

  • ReplaceAttributes A method that replaces the attributes of the current element with the content parameter. The content can be simple content, a collection of content objects, a parameter list of content objects, or null.

  • Save A method that outputs the current element’s XML tree to a file, Stream, TextWriter, or XmlWriter object.

  • SetElementValue A method that sets the value of a child element. The value is assigned to the first child element that matches the name parameter. If no child element with a matching name exists, a new child element is added. If the value is null, the first child element with the given name, if any, is deleted.

  • SetValue A method that assigns the Object parameter to the Value property of the current element by converting the given value to string.

  • WriteTo A method that writes the current element to the XmlWriter parameter.

The XDocument Class

The XDocument class represents an XML document that can contain a DTD, one root XML element, zero-to-many XML comments, and zero-to-many XML processing instructions. The XDocument class derives from XContainer and therefore inherits the members of XContainer, XNode, and XObject. The following are members of the XDocument class.

  • Declaration A read/write property of type XDeclaration. The XDeclaration class contains properties for Encoding, Standalone, and Version.

  • DocumentType A read-only property of XDocumentType, which is the Document Type Definition (DTD).

  • NodeType A read-only property that returns XmlNodeType.Document.

  • Root A read-only property that returns a reference to the first XML element in the XML document. The property returns Nothing (C# null) if there is no root element.

  • Load A shared (C# static) method that creates a new XDocument object and initializes it with the contents passed into the method. There are eight overloads for this method that enable you to load content from a Stream, TextReader, XmlReader, or string URI.

  • Parse A shared (C# static) method that creates a new XDocument object and initializes it with the XML passed as a string parameter. There is also an overload for this method that enables you to pass load options to preserve white space.

  • Save A method that outputs the current document’s XML tree to a file or XmlWriter.

  • WriteTo A method that writes the current document to a Stream, XmlWriter, TextWriter, or file.

Using the XDocument Classes

The previous introduction to the XDocument classes described the members of the more common classes, whereas this section will implement the XDocument family of classes.

The XDocument class can be populated very easily by using the constructor, the Load method, or the Parse method. The following code sample uses the Parse method to load an XML string into an XDocument object. After that, the Save method saves the XML document to the XDocumentTest.xml file.

Sample of Visual Basic Code

Private Sub ParseXDocumentToolStripMenuItem_Click( _
      ByVal sender As System.Object, ByVal e As System.EventArgs) _
      Handles ParseXDocumentToolStripMenuItem.Click
   Dim xml = _
         "<CustomersOrders>" & _
         "   <Customer CustomerID='ALFKI' CompanyName='Alfreds Futterkiste'>" & _
         "      <Order OrderID='10643' Freight='29.4600'/>" & _
         "      <Order OrderID='10692' Freight='61.0200'/>" & _
         "      <Order OrderID='10702' Freight='23.9400'/>" & _
         "      <Order OrderID='10835' Freight='69.5300'/>" & _
         "      <Order OrderID='10952' Freight='40.4200'/>" & _
         "      <Order OrderID='11011' Freight='1.2100'/>" & _
         "   </Customer>" & _
         "   <Customer CustomerID='ANATR' CompanyName='Ana Trujillo'>" & _
         "      <Order OrderID='10308' Freight='1.6100'/>" & _
         "      <Order OrderID='10625' Freight='43.9000'/>" & _
         "      <Order OrderID='10759' Freight='11.9900'/>" & _
         "      <Order OrderID='10926' Freight='39.9200'/>" & _
         "   </Customer>" & _
         "   <Customer CustomerID='ANTON' CompanyName='Antonio Moreno'>" & _
         "      <Order OrderID='10365' Freight='22.0000'/>" & _
         "      <Order OrderID='10507' Freight='47.4500'/>" & _
         "      <Order OrderID='10535' Freight='15.6400'/>" & _
         "      <Order OrderID='10573' Freight='84.8400'/>" & _
         "      <Order OrderID='10677' Freight='4.0300'/>" & _
         "      <Order OrderID='10682' Freight='36.1300'/>" & _
         "      <Order OrderID='10856' Freight='58.4300'/>" & _
         "   </Customer>" & _
         "   <Customer CustomerID='AROUT' CompanyName='Around the Horn'>" & _
         "      <Order OrderID='10355' Freight='41.9500'/>" & _
         "      <Order OrderID='10383' Freight='34.2400'/>" & _
         "      <Order OrderID='10453' Freight='25.3600'/>" & _
         "      <Order OrderID='10558' Freight='72.9700'/>" & _
         "      <Order OrderID='10707' Freight='21.7400'/>" & _
         "      <Order OrderID='10741' Freight='10.9600'/>" & _
         "      <Order OrderID='10743' Freight='23.7200'/>" & _
         "      <Order OrderID='10768' Freight='146.3200'/>" & _
         "      <Order OrderID='10793' Freight='4.5200'/>" & _
         "      <Order OrderID='10864' Freight='3.0400'/>" & _
         "      <Order OrderID='10920' Freight='29.6100'/>" & _
         "      <Order OrderID='10953' Freight='23.7200'/>" & _
         "      <Order OrderID='11016' Freight='33.8000'/>" & _
         "   </Customer>" & _
         "</CustomersOrders>"
   Dim doc = XDocument.Parse(xml)
   doc.Save(getFilePath("XDocumentTest.xml"))
   MessageBox.Show("XDocument Saved")
End Sub

Sample of C# Code

private void parseXDocumentToolStripMenuItem_Click(
   object sender, EventArgs e)
{
   string xml = @"
      <CustomersOrders>
         <Customer CustomerID='ALFKI' CompanyName='Alfreds Futterkiste'>
            <Order OrderID='10643' Freight='29.4600' />
            <Order OrderID='10692' Freight='61.0200' />
            <Order OrderID='10702' Freight='23.9400' />
            <Order OrderID='10835' Freight='69.5300' />
            <Order OrderID='10952' Freight='40.4200' />
            <Order OrderID='11011' Freight='1.2100' />
         </Customer>
         <Customer CustomerID='ANATR' CompanyName='Ana Trujillo'>
            <Order OrderID='10308' Freight='1.6100' />
            <Order OrderID='10625' Freight='43.9000' />
            <Order OrderID='10759' Freight='11.9900' />
            <Order OrderID='10926' Freight='39.9200' />
         </Customer>
         <Customer CustomerID='ANTON' CompanyName='Antonio Moreno'>
            <Order OrderID='10365' Freight='22.0000' />
            <Order OrderID='10507' Freight='47.4500' />
            <Order OrderID='10535' Freight='15.6400' />
            <Order OrderID='10573' Freight='84.8400' />
            <Order OrderID='10677' Freight='4.0300' />
            <Order OrderID='10682' Freight='36.1300' />
            <Order OrderID='10856' Freight='58.4300' />
         </Customer>
         <Customer CustomerID='AROUT' CompanyName='Around the Horn'>
            <Order OrderID='10355' Freight='41.9500' />
            <Order OrderID='10383' Freight='34.2400' />
            <Order OrderID='10453' Freight='25.3600' />
            <Order OrderID='10558' Freight='72.9700' />
            <Order OrderID='10707' Freight='21.7400' />
            <Order OrderID='10741' Freight='10.9600' />
            <Order OrderID='10743' Freight='23.7200' />
            <Order OrderID='10768' Freight='146.3200' />
            <Order OrderID='10793' Freight='4.5200' />
            <Order OrderID='10864' Freight='3.0400' />
            <Order OrderID='10920' Freight='29.6100' />
            <Order OrderID='10953' Freight='23.7200' />
            <Order OrderID='11016' Freight='33.8000' />
         </Customer>
      </CustomersOrders>
   ";
   var doc = XDocument.Parse(xml);
   doc.Save(getFilePath("XDocumentTest.xml"));
   MessageBox.Show("XDocument Saved");
}

Looking at the sample code, you might like the C# code better than the Visual Basic code because C# enables you to create a string that spans many rows. Visual Basic has an alternate way to load XML into an XDocument, as shown in the following code sample, which implements the XDocument constructor.

Sample of Visual Basic Code

Private Sub XDocumentConstructorToolStripMenuItem_Click( _
      ByVal sender As System.Object, ByVal e As System.EventArgs) _
      Handles XDocumentConstructorToolStripMenuItem.Click
   Dim doc As XDocument = _
      <?xml version="1.0" encoding="utf-8" standalone="yes"?>
      <CustomersOrders>
         <Customer CustomerID='ALFKI' CompanyName='Alfreds Futterkiste'>
            <Order OrderID='10643' Freight='29.4600'/>
            <Order OrderID='10692' Freight='61.0200'/>
            <Order OrderID='10702' Freight='23.9400'/>
            <Order OrderID='10835' Freight='69.5300'/>
            <Order OrderID='10952' Freight='40.4200'/>
            <Order OrderID='11011' Freight='1.2100'/>
         </Customer>
         <Customer CustomerID='ANATR' CompanyName='Ana Trujillo'>
            <Order OrderID='10308' Freight='1.6100'/>
            <Order OrderID='10625' Freight='43.9000'/>
            <Order OrderID='10759' Freight='11.9900'/>
            <Order OrderID='10926' Freight='39.9200'/>
         </Customer>
         <Customer CustomerID='ANTON' CompanyName='Antonio Moreno'>
            <Order OrderID='10365' Freight='22.0000'/>
            <Order OrderID='10507' Freight='47.4500'/>
            <Order OrderID='10535' Freight='15.6400'/>
            <Order OrderID='10573' Freight='84.8400'/>
            <Order OrderID='10677' Freight='4.0300'/>
            <Order OrderID='10682' Freight='36.1300'/>
            <Order OrderID='10856' Freight='58.4300'/>
         </Customer>
         <Customer CustomerID='AROUT' CompanyName='Around the Horn'>
            <Order OrderID='10355' Freight='41.9500'/>
            <Order OrderID='10383' Freight='34.2400'/>
            <Order OrderID='10453' Freight='25.3600'/>
            <Order OrderID='10558' Freight='72.9700'/>
            <Order OrderID='10707' Freight='21.7400'/>
            <Order OrderID='10741' Freight='10.9600'/>
            <Order OrderID='10743' Freight='23.7200'/>
            <Order OrderID='10768' Freight='146.3200'/>
            <Order OrderID='10793' Freight='4.5200'/>
            <Order OrderID='10864' Freight='3.0400'/>
            <Order OrderID='10920' Freight='29.6100'/>
            <Order OrderID='10953' Freight='23.7200'/>
            <Order OrderID='11016' Freight='33.8000'/>
         </Customer>
      </CustomersOrders>
   doc.Save(getFilePath("XDocumentTest.xml"))
   MessageBox.Show("XDocument Saved")
End Sub

Sample of C# Code

private void xDocumentConstructorToolStripMenuItem_Click(
   object sender, EventArgs e)
{
   var doc = new XDocument(
      new XElement("CustomersOrders",
         new XElement("Customer",
            new XAttribute("CustomerID", "ALFKI"),
            new XAttribute("CompanyName", "Alfreds Futterkiste"),
            new XElement("Order",
                        new XAttribute("OrderID", "10643"),
                        new XAttribute("Freight", "29.4600")),
            new XElement("Order",
                        new XAttribute("OrderID", "10692"),
                        new XAttribute("Freight", "61.0200")),
            new XElement("Order",
                        new XAttribute("OrderID", "10702"),
                        new XAttribute("Freight", "23.9400")),
            new XElement("Order",
                        new XAttribute("OrderID", "10835"),
                        new XAttribute("Freight", "69.5300")),
            new XElement("Order",
                        new XAttribute("OrderID", "10952"),
                        new XAttribute("Freight", "40.4200")),
            new XElement("Order",
                        new XAttribute("OrderID", "11011"),
                        new XAttribute("Freight", "1.2100"))),
         new XElement("Customer",
            new XAttribute("CustomerID", "ANATR"),
            new XAttribute("CompanyName", "Ana Trujillo"),
            new XElement("Order",
                        new XAttribute("OrderID", "10308"),
                        new XAttribute("Freight", "1.6100")),
            new XElement("Order",
                        new XAttribute("OrderID", "10625"),
                        new XAttribute("Freight", "43.9000")),
            new XElement("Order",
                        new XAttribute("OrderID", "10759"),
                        new XAttribute("Freight", "11.9900")),
            new XElement("Order",
                        new XAttribute("OrderID", "10926"),
                        new XAttribute("Freight", "39.9200"))),
         new XElement("Customer",
            new XAttribute("CustomerID", "ANTON"),
            new XAttribute("CompanyName", "Antonio Moreno"),
            new XElement("Order",
                        new XAttribute("OrderID", "10365"),
                        new XAttribute("Freight", "22.0000")),
            new XElement("Order",
                        new XAttribute("OrderID", "10507"),
                        new XAttribute("Freight", "47.4500")),
            new XElement("Order",
                        new XAttribute("OrderID", "10535"),
                        new XAttribute("Freight", "15.6400")),
            new XElement("Order",
                        new XAttribute("OrderID", "10573"),
                        new XAttribute("Freight", "84.8400")),
            new XElement("Order",
                        new XAttribute("OrderID", "10677"),
                        new XAttribute("Freight", "4.0300")),
            new XElement("Order",
                        new XAttribute("OrderID", "10682"),
                        new XAttribute("Freight", "36.1300")),
            new XElement("Order",
                        new XAttribute("OrderID", "10856"),
                        new XAttribute("Freight", "58.5300"))),
         new XElement("Customer",
            new XAttribute("CustomerID", "AROUT"),
            new XAttribute("CompanyName", "Around the Horn"),
            new XElement("Order",
                        new XAttribute("OrderID", "10355"),
                        new XAttribute("Freight", "41.9500")),
            new XElement("Order",
                        new XAttribute("OrderID", "10383"),
                        new XAttribute("Freight", "34.2400")),
            new XElement("Order",
                        new XAttribute("OrderID", "10453"),
                        new XAttribute("Freight", "25.3600")),
            new XElement("Order",
                        new XAttribute("OrderID", "10558"),
                        new XAttribute("Freight", "72.9700")),
            new XElement("Order",
                        new XAttribute("OrderID", "10707"),
                        new XAttribute("Freight", "21.7400")),
            new XElement("Order",
                        new XAttribute("OrderID", "10741"),
                        new XAttribute("Freight", "10.9600")),
            new XElement("Order",
                        new XAttribute("OrderID", "10743"),
                        new XAttribute("Freight", "23.7200")),
            new XElement("Order",
                        new XAttribute("OrderID", "10768"),
                        new XAttribute("Freight", "146.3200")),
            new XElement("Order",
                        new XAttribute("OrderID", "10793"),
                        new XAttribute("Freight", "4.5200")),
            new XElement("Order",
                        new XAttribute("OrderID", "10864"),
                        new XAttribute("Freight", "3.0400")),
            new XElement("Order",
                        new XAttribute("OrderID", "10920"),
                        new XAttribute("Freight", "29.6100")),
            new XElement("Order",
                        new XAttribute("OrderID", "10953"),
                        new XAttribute("Freight", "23.7200")),
            new XElement("Order",
                        new XAttribute("OrderID", "11016"),
                        new XAttribute("Freight", "33.8000")))));
   doc.Save(getFilePath("XDocumentTest.xml"));
   MessageBox.Show("XDocument Saved");
}

In this code sample, the Visual Basic code is much simpler than the C# code. The Visual Basic compiler parses the XML literals and creates the same code that the C# compiler creates. This is very different from the previous example, in which the Parse method parsed an XML string. In this example, the code runs faster than the Parse example because there is no need to parse at run time.

Implementing LINQ to XML Queries

You can use many of the techniques covered in Chapter 3, “Introducing LINQ”, to query an XDocument or XElement object. By using the XML from the previous examples, you might want to return the freight and customer ID for order ID 10677, which can be accomplished as shown in the following code sample:

Sample of Visual Basic Code

Private Sub LINQQueryToolStripMenuItem_Click( _
      ByVal sender As System.Object, ByVal e As System.EventArgs) _
      Handles LINQQueryToolStripMenuItem.Click
   Dim doc = XDocument.Load(getFilePath("XDocumentTest.xml"))
   Dim result = (From order In doc.Descendants("Order")
                 Where order.Attribute("OrderID").Value = "10677"
                 Select New With
                 {
                    .OrderID = CType(order.Attribute("OrderID"), Integer),
                    .CustomerID = CType(order.Parent.Attribute("CustomerID"), String),
                    .Freight = CType(order.Attribute("Freight"), Decimal)
                 }).FirstOrDefault()
   txtLog.Text = String.Format("OrderID:{0}  CustomerID:{1}  Freigth:{2:C}", _
      result.OrderID, result.CustomerID, result.Freight)
End Sub

Sample of C# Code

private void lINQQueryToolStripMenuItem_Click(
   object sender, EventArgs e)
{
   var doc = XDocument.Load(getFilePath("XDocumentTest.xml"));
   var result = (from order in doc.Descendants("Order")
                  where order.Attribute("OrderID").Value == "10677"
                  select new
                           {
                              OrderID=(int)order.Attribute("OrderID"),
                              CustomerID = (string)order.Parent.Attribute("CustomerID"),
                              Freight = (decimal)order.Attribute("Freight")
                           }).FirstOrDefault();
   txtLog.Text = string.Format("OrderID:{0}  CustomerID:{1}  Freigth:{2:C}",
      result.OrderID, result.CustomerID, result.Freight);
}

In this code sample, the Load method of the XDocument class is executed to retrieve the XML document from the previous examples, and then a LINQ query is applied to the XDocument object. The select clause creates an anonymous type containing OrderID, CustomerID, and Freight. The FirstOrDefault query extension method is implemented to return a single object of the anonymous type. The result is then displayed in the txtLog text box.

In the code sample, CType (C# explicit cast) converts the attribute to the appropriate type. The XAttribute class has 25 explicit conversion operators to convert the XAttribute object to a different .NET Framework type. This feature simplifies the syntax of the LINQ query.

Using LINQ to XML with Aggregates

If you want to query for the sum of the freight for each customer, you can use LINQ to XML as shown in the following code sample:

Sample of Visual Basic Code

Private Sub LINQQuerySumToolStripMenuItem_Click( _
      ByVal sender As System.Object, ByVal e As System.EventArgs) _
      Handles LINQQuerySumToolStripMenuItem.Click
   Dim doc = XDocument.Load(getFilePath("XDocumentTest.xml"))
   Dim result = From customer In doc.Descendants("Customer")
         Select New With
         {
            .CustomerID = CType(customer.Attribute("CustomerID"), String),
            .TotalFreight = customer.Descendants("Order") _
               .Sum(Function(o) CType(o.Attribute("Freight"), Decimal))
         }
   txtLog.Clear()
   For Each customer In result
      txtLog.AppendText( _
         String.Format("CustomerID:{0}  TotalFreight:{1,8:C}" + vbCrLf, _
            customer.CustomerID, customer.TotalFreight))
   Next
End Sub

Sample of C# Code

private void lINQQuerySumToolStripMenuItem_Click(
   object sender, EventArgs e)
{
   var doc = XDocument.Load(getFilePath("XDocumentTest.xml"));
   var result = from customer in doc.Descendants("Customer")
               select new
               {
                  CustomerID = (string)customer.Attribute("CustomerID"),
                  TotalFreight = customer.Descendants("Order")
                     .Sum(o=>(decimal)o.Attribute("Freight"))
               };
   txtLog.Clear();
   foreach (var customer in result)
   {
      txtLog.AppendText( string.Format("CustomerID:{0}  TotalFreight:{1,8:C}\r\n",
         customer.CustomerID, customer.TotalFreight));
   }
}

The sample code starts by loading the XML file into an XDocument object. Next, a LINQ to XML query is created that retrieves Customer elements, but the select clause creates an anonymous type containing CustomerID and TotalFreight. TotalFreight is calculated by retrieving the Order elements of each customer and then executes the Sum query extension method by which the Freight attribute is converted to a decimal type to perform the aggregation.

Using LINQ to XML Joins

Joins can be accomplished between LINQ to XML and other LINQ providers such as LINQ to Objects. The following code sample starts with an array of orders to be retrieved. This array could have come from a multiple selection list box or some other customer input screen. The order array is then joined to the XML file that was used in the previous examples to retrieve the desired order information.

Sample of Visual Basic Code

Private Sub LINQQuerySumToolStripMenuItem_Click( _
      ByVal sender As System.Object, ByVal e As System.EventArgs) _
      Handles LINQQuerySumToolStripMenuItem.Click
   Dim orders() = {"10707", "10835", "10953"}
   Dim doc = XDocument.Load(getFilePath("XDocumentTest.xml"))
   Dim result = From order In doc.Descendants("Order")
                Join selected In orders On
                CType(order.Attribute("OrderID"), String) Equals selected
         Select New With
         {
            .OrderID = CType(order.Attribute("OrderID"), Integer),
            .CustomerID = CType(order.Parent.Attribute("CustomerID"), String),
            .Freight = CType(order.Attribute("Freight"), Decimal)
         }
   txtLog.Clear()
   For Each order In result
      txtLog.AppendText( _
         String.Format("OrderID:{0} CustomerID:{1}  TotalFreight:{2:C}" + vbCrLf, _
            order.OrderID, order.CustomerID, order.Freight))
   Next
End Sub

Sample of C# Code

private void lINQQueryJoinToolStripMenuItem_Click(
   object sender, EventArgs e)
{
   string[] orders = {"10707","10835","10953"};

   var doc = XDocument.Load(getFilePath("XDocumentTest.xml"));
   var result = from order in doc.Descendants("Order")
                  join selected in orders
                     on (string) order.Attribute("OrderID") equals selected
                  select new
               {
                  OrderID = (int) order.Attribute("OrderID"),
                  CustomerID = (string) order.Parent.Attribute("CustomerID"),
                  Freight = (decimal) order.Attribute("Freight")
               };
   txtLog.Clear();
   foreach (var order in result)
   {
      txtLog.AppendText(
         string.Format("OrderID:{0}  CustomerID:{1}  Freight:{2:C}\r\n",
                     order.OrderID, order.CustomerID, order.Freight));
   }
}

Using LINQ to XML with Namespaces

LINQ to XML supports the use of namespaces, also known as the namespace URI, in addition to the local name of an XML node. Like .NET Framework namespaces, XML namespaces avoid naming collisions, especially when combining multiple XML documents that might have nodes that have the same name but different meanings. For example, a Title element would have a different meaning if it refers to a book and is compared to the Title element that refers to a person.

When working with XML namespaces, you can assign a prefix to the namespace. The prefixes can be the source of many problems because prefixes are scoped to their context, so a prefix of abc can be associated with namespace x in one part of the XML document and can be associated with namespace y in a different part of the document.

The following code sample uses an XML document that contains namespace definitions. Three queries are run against it.

Sample of Visual Basic Code

Private Sub LINQQueryNamespaceToolStripMenuItem_Click( _
       ByVal sender As System.Object, ByVal e As System.EventArgs) _
       Handles LINQQueryNamespaceToolStripMenuItem.Click
   Dim xml = "<Root xmlns:aw='http://www.adventure-works.com' " & _
             "     xmlns='http://www.xyz.com'> " & _
             "   <Child>1</Child> " & _
             "   <aw:Child>2</aw:Child> " & _
             "   <Child>3</Child> " & _
             "   <aw:Child>4</aw:Child> " & _
             "   <Child>5</Child> " & _
            "   <aw:Child>6</aw:Child> " & _
            "</Root>"
   Dim doc = XDocument.Parse(xml)
   txtLog.Clear()

   Dim result1 = From c In doc.Descendants("Child")
                 Select c
   txtLog.AppendText("Query for Child\r\n")
   For Each xElement In result1
      txtLog.AppendText(CType(xElement, String) + vbCrLf)
   Next

   Dim aw = XNamespace.Get("http://www.adventure-works.com")
   Dim result2 = From c In doc.Descendants(aw + "Child")
                 Select c
   txtLog.AppendText("Query for aw+Child" + vbCrLf)
   For Each xElement In result2
      txtLog.AppendText(CType(xElement, String) + vbCrLf)
   Next

   Dim defaultns = XNamespace.Get("http://www.xyz.com")
   Dim result3 = From c In doc.Descendants(defaultns + "Child")
                 Select c
   txtLog.AppendText("Query for defaultns+Child\r\n")
   For Each xElement In result3

      txtLog.AppendText(CType(xElement, String) + vbCrLf)
   Next

   txtLog.AppendText("Done" + vbCrLf)
End Sub

Sample of C# Code

private void lINQQueryNamespaceToolStripMenuItem_Click(
   object sender, EventArgs e)
{
   var xml =
      @"<Root xmlns:aw='http://www.adventure-works.com'
               xmlns='http://www.xyz.com'>
               <Child>1</Child>
               <aw:Child>2</aw:Child>
               <Child>3</Child>
               <aw:Child>4</aw:Child>
               <Child>5</Child>
               <aw:Child>6</aw:Child>
         </Root>";

   var doc = XDocument.Parse(xml);
   txtLog.Clear();

   var result1 = from c in doc.Descendants("Child")
                  select c;
   txtLog.AppendText("Query for Child\r\n");
   foreach (var xElement in result1)
   {
      txtLog.AppendText((string)xElement + "\r\n");
   }

   var aw = XNamespace.Get("http://www.adventure-works.com");
   var result2 = from c in doc.Descendants(aw + "Child")
                  select c;
   txtLog.AppendText("Query for aw+Child\r\n");
   foreach (var xElement in result2)
   {
      txtLog.AppendText((string)xElement + "\r\n");
   }

   var defaultns = XNamespace.Get("http://www.xyz.com");
   var result3 = from c in doc.Descendants(defaultns + "Child")
                  select c;
   txtLog.AppendText("Query for defaultns+Child\r\n");
   foreach (var xElement in result3)
   {
      txtLog.AppendText((string)xElement + "\r\n");
   }

   txtLog.AppendText("Done\r\n");
}

In this code sample, the XDocument class is being populated by passing the XML string parameter to the Parse method, and the txtLog text box is cleared.

Next, the first query that produced result1 is displayed. This query does not produce any elements because the query is looking for Child elements that are not in any namespace. In this example, all the Child elements are in a namespace; even the Child nodes that have no prefix are in the default namespace (http://www.xyz.com).

The second query is for the Child elements in the namespace defined by the aw prefix. For this example, an aw variable is created and assigned to the http://www.adventure-works.com names 2, 4, and 6.

The third query is for the Child elements in the default namespace. For this example, a defaultns variable is created and assigned to the http://www.xyz.com namespace. You can use the defaultns variable by adding the defaultns prefix to the local name (Child) by using the plus sign before the local name. This query produces the result3 variable that enumerates as 1, 3, and 5.

Practice: Work with the XDocument Class

This practice is a continuation of the practice exercises in Lesson 1. In this practice, you analyze an XML file, called Orders.xml, which contains order information. Your first objective is to write a program that can provide the total price of all orders. You also need to provide the total and the average freight cost per order. In this exercise, you use the XDocument class and measure its performance by using the Stopwatch class.

EXERCISE Implementing the XDocument Solution

In this exercise, you extend the Console Application project from Lesson 1 by adding code to retrieve the necessary data by using the XmlReader class.

  1. In Visual Studio .NET 2010, choose File | Open | Project.

  2. Select the project you created in Lesson 1 or open the project in the Begin folder for Lesson 2.

  3. In Main, after the call to parseWithXmlReader, add a parseWithXDocument method and pass the file name as a parameter. Add this method to your code. Your code should look like the following:

    Sample of Visual Basic Code

    Sub Main()
       Dim fileName = "Orders.xml"
       parseWithXmlDocument(fileName)
       parseWithXmlReader(fileName)
       parseWithXDocument(fileName)
       Console.Write("Press <Enter> to end")
       Console.ReadLine()
    End Sub
    
    Private Sub parseWithXDocument (ByVal fileName As String)
    
    End Sub

    Sample of C# Code

    static void Main(string[] args)
    {
       string fileName = "Orders.xml";
       parseWithXmlDocument(fileName);
       parseWithXmlReader(fileName);
       parseWithXDocument(fileName);
       Console.Write("Press <Enter> to end");
       Console.ReadLine();
    }
    
    private static void parseWithXDocument(string fileName)
    {
    
    }
  4. In the parseWithXDocument method, instantiate a Stopwatch, assign the object to a variable, and start it. Declare variables for the total order price, the total freight cost, the average freight cost, and the order count. Add a line of code to load XDocument into memory. Add imports System.Xml.Linq (C# using System.Xml.Linq;) to the top of your file. Your code should look like the following:

    Sample of Visual Basic Code

    Private Sub parseWithXDocument(ByVal fileName As String)
          Dim sw = New Stopwatch()
          sw.Start()
          Dim totalOrderPrice As Decimal = 0
          Dim totalFreightCost As Decimal = 0
          Dim orderQty As Integer = 0
          Dim doc = XDocument.Load(fileName)
    
    End Sub

    Sample of C# Code

    private static void parseWithXDocument(string fileName)
    {
       var sw = new Stopwatch();
       sw.Start();
       decimal totalOrderPrice = 0;
       decimal totalFreightCost = 0;
       decimal orderQty = 0;
       var doc = XDocument.Load(fileName);
    
    }
  5. Add a for each (C# foreach) loop to iterate over all Order elements. In the loop, increment the order quantity variable and add a nested for each (C# foreach) loop to iterate over all the LineItem elements of the current Order. In the nested loop, add code to retrieve the quantity, price, and freight from the line item. Add the freight to the total freight cost and add the line item cost to the total order price. Your code should look like the following:

    Sample of Visual Basic Code

    For Each order In doc.Descendants("Order")
       orderQty += 1
       For Each lineItem In order.Descendants("LineItem")
          Dim qty = CType(lineItem.Attribute("Qty"), Decimal)
          Dim price = CType(lineItem.Attribute("Price"), Decimal)
          Dim freight = CType(lineItem.Attribute("Freight"), Decimal)
          totalFreightCost += freight
          totalOrderPrice += (qty * price) + freight
       Next
    Next

    Sample of C# Code

    foreach (var order in doc.Descendants("Order"))
    {
       ++orderQty;
       foreach (var lineItem in order.Descendants("LineItem"))
       {
          var qty = (decimal)lineItem.Attribute("Qty");
          var price = (decimal)lineItem.Attribute("Price");
          var freight = (decimal)lineItem.Attribute("Freight");
          totalFreightCost += freight;
          totalOrderPrice += (qty * price) + freight;
       }
    }
  6. Add code to display the total order price, the total freight cost, and the average freight cost per order. Stop the stopwatch and display the elapsed time. Your completed method should look like the following:

    Sample of Visual Basic Code

    Private Sub parseWithXDocument(ByVal fileName As String)
       Dim sw = New Stopwatch()
       sw.Start()
       Dim totalOrderPrice As Decimal = 0
       Dim totalFreightCost As Decimal = 0
       Dim orderQty As Integer = 0
       Dim doc = XDocument.Load(fileName)
    
       For Each order In doc.Descendants("Order")
          orderQty += 1
          For Each lineItem In order.Descendants("LineItem")
             Dim qty = CType(lineItem.Attribute("Qty"), Decimal)
             Dim price = CType(lineItem.Attribute("Price"), Decimal)
             Dim freight = CType(lineItem.Attribute("Freight"), Decimal)
             totalFreightCost += freight
             totalOrderPrice += (qty * price) + freight
          Next
       Next
    
       Console.WriteLine("Total Order Price: {0:C}", totalOrderPrice)
       Console.WriteLine("Total Freight Cost: {0:C}", totalFreightCost)
       Console.WriteLine("Average Freight Cost per Order: {0:C}", _
                         totalFreightCost / orderQty)
       sw.Stop()
       Console.WriteLine("Time to Parse XDocument: {0}", sw.Elapsed)
    End Sub

    Sample of C# Code

    private static void parseWithXDocument(string fileName)
    {
       var sw = new Stopwatch();
       sw.Start();
       decimal totalOrderPrice = 0;
       decimal totalFreightCost = 0;
       decimal orderQty = 0;
       var doc = XDocument.Load(fileName);
    
       foreach (var order in doc.Descendants("Order"))
       {
          ++orderQty;
          foreach (var lineItem in order.Descendants("LineItem"))
          {
             var qty = (decimal)lineItem.Attribute("Qty");
             var price = (decimal)lineItem.Attribute("Price");
             var freight = (decimal)lineItem.Attribute("Freight");
             totalFreightCost += freight;
             totalOrderPrice += (qty * price) + freight;
          }
       }
       Console.WriteLine("Total Order Price: {0:C}", totalOrderPrice);
       Console.WriteLine("Total Freight Cost: {0:C}", totalFreightCost);
       Console.WriteLine("Average Freight Cost per Order: {0:C}",
          totalFreightCost / orderQty);
    
       sw.Stop();
       Console.WriteLine("Time to Parse XDocument: {0}", sw.Elapsed);
    }
  7. Run the application. Your total time will vary based on your machine configuration, but your output should look like the following:

    Result

    Total Order Price: $82,989,370.79
    Total Freight Cost: $2,011,265.92
    Average Freight Cost per Order: $529.84
    Time to Parse XmlDocument: 00:00:00.7383706
    Total Order Price: $82,989,370.79
    Total Freight Cost: $2,011,265.92
    Average Freight Cost per Order: $529.84
    Time to Parse XmlReader: 00:00:00.3213059
    Total Order Price: $82,989,370.79
    Total Freight Cost: $2,011,265.92
    Average Freight Cost per Order: $529.84
    Time to Parse XDocument: 00:00:00.4891875
    Press <Enter> to end

After coding and running this application, you see the results. The fastest test was XmlReader, after which are XDocument and XmlDocument. Because XDocument provides more capabilities using LINQ to XML, you probably want to use XDocument in most scenarios except when performance is most important.

Lesson Summary

This lesson provided detailed information about the XDocument class family.

  • The XDocument class provides in-memory, random, read-write access to an XML document.

  • The XDocument class provides access to the node by using LINQ to XML classes.

  • When working with XAttribute objects, you can retrieve a typed value by using the CType (C# explicit cast) statement to convert the attribute value to the desired type.

  • When working with XElement objects, you can retrieve a typed value by using the CType (C# explicit cast) statement to convert the element value to the desired type.

  • The XDocument and XElement classes provide Load methods for loading from an XmlReader file.

  • The XDocument and XElement classes provide a constructor that enables you to pass in XNode classes representing the content. The Visual Basic compiler enables you to specify the XML as a string, and it parses the string and generates statements to create the appropriate XElement and XAttribute objects.

  • The XDocument and XElement classes provide a Parse method that enables you to pass in an XML string that will be parsed into the appropriate XML content.

Lesson Review

You can use the following questions to test your knowledge of the information in Lesson 2, “Querying with LINQ to XML”. The questions are also available on the companion CD if you prefer to review them in electronic form.

  1. Given an XML file, you want to run several queries for data using LINQ to XML. Which class would be most appropriate for these queries on the file?

    1. XmlDocument

    2. XmlReader

    3. XDocument

  2. In your code, you have a string variable that contains XML. Which method can you use to convert this into an XDocument class so you can run LINQ to XML queries?

    1. Load

    2. Constructor

    3. WriteTo

    4. Parse