Using Functional Construction to Create XML Documents

Introduction

I heard through the grapevine that VB.NET was mentioned on an episode of CSI: recently. Because I don't watch much TV, I missed it, but it's cool. It's a little like the invisible hand of Bill Gates reached out and said let's make VB.NET cool again. (Did VB ever stop being cool?) Or, maybe one of CSI:'s writers is a closet VB programmer. That's cool, too.

Recently, you read about VB supporting literal XML in my article "Using Literal XML with Embedded Expressions in VB9." In this article, you will read about an aspect of LINQ to XML referred to as functional construction. Functional construction is a way of dynamically creating XML documents through simple method calls.

Constructing an XML Document with Function Calls

The System.Xml.Linq namespace contains LINQ to XML classes such as XDocument, XElement, and XAttribute. (There are other class representing CDATA, comments, declarations, text, and processing instructions, too.) By creating instances of these classes and passing in the names and values, you can convert any data into an XML document. Constructors are essentially functions and this capability is referred to as functional construction. Once you see some code representing functional construction, you will pleased at how easy it is.

First, however, you will need some data to convert to XML. To that end and to provide a complete sample solution, there are several techniques demonstrated by the sample code, including boilerplate ADO.NET code to move database data into custom entity objects. The solution is decomposed in the sub-sections that follow and the entire solution is provided as one listing at the end of the article.

Defining a Custom Entity Class

An entity class is a class that maps to a database table, generally. Entity classes can encompass data retrieved from a view or stored procedure too. The basic idea is that a class contains properties that map one-to-one by name and type to columns in a table.

Note: In practice, the code in this part can and should be replaced with LINQ to SQL code, which follows roughly the same principles but is even easier to implement.

The code in Listing 1 contains an entity the AdventureWorks Customer table. (I guess even the Microsoft folks got tired of Northwind Traders, which doesn't ship with SQL Server 2005, and so there is a subtle movement to supplant it with the more advanced AdventureWorks database.)

Listing 1: An entity class mapped to the AdventureWorks Customer table.

Public Class Customer
   Private _customerID As Integer
   Public Property CustomerID() As Integer
      Get
         Return _customerID
      End Get
      Set(ByVal Value AsInteger)
         _customerID = Value
      End Set
   End Property

   Private _territoryID As Integer
   Public Property TerritoryID() As Integer
      Get
         Return _territoryID
      End Get
      Set(ByVal Value As Integer)
         _territoryID = Value
      End Set
   End Property

   Private _accountNumber As String
   Public Property AccountNumber() As String
      Get
         Return _accountNumber
      End Get
      Set(ByVal Value As String)
         _accountNumber = Value
      End Set
   End Property

   Private _customerType As String
   Public Property CustomerType() As String
      Get
         Return _customerType
      End Get
      Set(ByVal Value As String)
         _customerType = Value
      End Set
   End Property

   Private _rowGuid As Guid
   Public Property RowGuid() As Guid
      Get
         Return _rowGuid
      End Get
      Set(ByVal Value As Guid)
         _rowGuid = Value
      End Set
   End Property

   Private _modifiedDate As DateTime
   Public Property ModifiedDate() As DateTime
      Get
         Return _modifiedDate
      End Get
      Set(ByVal Value As DateTime)
         _modifiedDate = Value
      End Set
   End Property

End Class

Listing 1 is about as straightforward a class as possible, so go ahead and move on. The code in Listing 2 contains plain vanilla ADO.NET 2.0 that connects to the AdventureWorks database and selects all of the Customer rows. The ADO.NET code is shown in the GetCustomers function. GetCustomers returns a List(of Customer) objects (using the generic List class). GetCustomers has two helper extension methods, the factory method Create and the generic extension method SafeRead that handles checking for null fields.

Using Functional Construction to Create XML Documents

Listing 2: GetCustomers contains plain vanilla ADO.NET code but uses two helper extension methods for convenience.

Private Function GetCustomers() As List(Of Customer)
   Dim connectionString As String = _
      "Data Source=.\SQLExpress;Initial Catalog=AdventureWorks;" + _
      "Integrated Security=True"

   Dim customers As List(Of Customer) = New List(Of Customer)
   Using connection As SqlConnection = _
      New SqlConnection(connectionString)
      connection.Open()
      Dim command As SqlCommand = New SqlCommand( _
         "SELECT * FROM Sales.Customer", connection)

         Dim reader As SqlDataReader = command.ExecuteReader

         While (reader.Read())
            customers.Add(New Customer().Create(reader))
         End While

      End Using

      Return customers
   End Function

   <Extension()> _
   Public Function Create(ByVal customer As Customer, _
                          ByVal reader _
                          As SqlDataReader) As Customer
      customer.AccountNumber = customer.SafeRead(reader, _
         "AccountNumber", "")
      customer.CustomerID = customer.SafeRead(reader, _
         "CustomerID", -1)
      customer.TerritoryID = customer.SafeRead(reader, _
         "TerritoryID", -1)
      customer.CustomerType = customer.SafeRead(reader, _
         "CustomerType", "S")
      customer.RowGuid = customer.SafeRead(reader, "rowguid", _
         Guid.NewGuid())
      customer.ModifiedDate = customer.SafeRead(reader, _
         "ModifiedDate", DateTime.Now)

      Return customer
   End Function

   <Extension()> _
   Function SafeRead(Of T)(ByVal customer As Customer, _
      ByVal reader As SqlDataReader, ByVal fieldName As String, _
      ByVal defaultValue As T) As T

      Try
         Return Convert.ChangeType(reader(fieldName), _
            defaultValue.GetType())
      Catch ex As Exception
         Return defaultValue
      End Try

End Function

Create is an implementation of the factory pattern. A factory pattern is used to construct objects that have slightly complicated construction needs. In this case, the factory pattern was used because you need to get the Customer fields from the SqlDataReader. SafeReader is used to handle nulls. (In practice, add a check to see whether reader(fieldname) returns Nothing; this will prevent a few extra unnecessary exceptions.)

Extension methods are an implementation of the decorator pattern. By using the ExtensionAttribute on methods, you can avoid inheritance where it is inconvenient or impossible—as is the case with classes marked NotInheritable (or sealed in C#). Extension method permits you to program with member semantics against methods that are not literally members. The ExtensionAttribute marks a method as an extension method, and the first argument indicates the type being extended. For example, SafeRead has a first argument of Customer. This means you can call SafeRead as if it were a member of the Customer class.

Constructing the XML Document

The code you have seen so far all works toward getting a list of Customer objects. Now that you have that data, you easily can convert the List(Of Customer) objects into an XML document. The fragment in Listing does this for you.

Listing 3: Using LINQ and functional construction to convert a List(Of Customer) objects to an XML document.

New XElement("Customers", _
   From customer In list _
   Select New XElement("Customer", _
      New XElement("CustomerID", customer.CustomerID), _
      New XElement("TerritoryID", customer.TerritoryID), _
      New XElement("AccountNumber", customer.AccountNumber), _
      New XElement("rowguid", customer.RowGuid), _
      New XElement("ModifiedDate", customer.ModifiedDate)))

The first part of the fragment creates a new XElement object. Everything else is simply additional parameters supplied to that XElement constructor. (The XElement constructor supports an optional and arbitrary number of inputs through the ParamArray modifier.)

The additional arguments are supplied by the embedded LINQ query. The From customer In list defines a range value customer against a supplied list. (The list value will be the List(Of Customer) objects. The Select clause uses projection and functional construction to create subordinate XElement arguments for each of the Customer rows and columns. The result is an XML document that looks like this:

<Customers>
   <Customer>
      <CustomerID></CustomerID>
      <TerritoryID></TerritoryID>
      <AccountNumber></AccountNumber>
      <rowguid></rowguid>
      <ModifiedDate></ModifiedDate>
   </Customer>
   <Customer>
      ...
   </Customer>
</Customers>

Using Functional Construction to Create XML Documents

Listing 4: The complete sample program listing.

Imports System.Data
Imports System.Data.SqlClient
Imports System.Runtime.CompilerServices
Imports System.Text
Imports System.Reflection

Module Module1

   Sub Main()`
      Dim customers As List(Of Customer) = GetCustomers()
      'Console.WriteLine(customers.Take(5).Dump)
      'Console.ReadLine()

      Dim xml = ConstructXMLDocument(customers.Take(5))
      Console.WriteLine(xml)
      Console.ReadLine()
   End Sub

   Function ConstructXMLDocument(ByVal list _
      As IEnumerable(Of Customer)) _
      As XElement

      Return New XElement("Customers", _
         From customer In list _
         Select New XElement("Customer", _
            New XElement("CustomerID", customer.CustomerID), _
            New XElement("TerritoryID", customer.TerritoryID), _
            New XElement("AccountNumber", _
               customer.AccountNumber), _
            New XElement("rowguid", customer.RowGuid), _
            New XElement("ModifiedDate", customer.ModifiedDate)))

   End Function

   Private Function GetCustomers() As List(Of Customer)
      Dim connectionString As String = _
         "Data Source=.\SQLExpress;Initial Catalog=AdventureWorks;" + _
         "Integrated Security=True"

      Dim customers As List(Of Customer) = New List(Of Customer)
      Using connection As SqlConnection = _
         New SqlConnection(connectionString)
         connection.Open()
         Dim command As SqlCommand = New SqlCommand( _
            "SELECT * FROM Sales.Customer", connection)

         Dim reader As SqlDataReader = command.ExecuteReader

         While (reader.Read())
            customers.Add(New Customer().Create(reader))
         End While

      End Using

      Return customers
   End Function

   <Extension()> _
   Public Function Dump(ByVal list As IEnumerable(Of Customer)) _
      As String
      Dim builder As StringBuilder = New StringBuilder()
      For Each customer As Customer In list
         builder.Append(customer.Dump())
         builder.AppendLine()
      Next
      Return builder.ToString()
   End Function

   <Extension()> _
   Public Function Dump(ByVal customer As Customer) As String
      Dim builder As StringBuilder = New StringBuilder()
      Dim info() As PropertyInfo = _
         customer.GetType().GetProperties()

      For Each prop As Propertyinfo In info
         Try
            Console.WriteLine("Name: {0}, Value: {1}", _
                              prop.Name, _
                              prop.GetValue(customer, Nothing))

         Catch ex As Exception
            Console.WriteLine("Name: {0}, Value: {1}", _
                              prop.Name, "none")
         End Try
      Next

      Return builder.ToString()

   End Function

   <Extension()> _
   Public Function Create(ByVal customer As Customer, _
                          ByVal reader _
                          As SqlDataReader) As Customer

      customer.AccountNumber = customer.SafeRead(reader, _
         "AccountNumber", "")
      customer.CustomerID = customer.SafeRead(reader, _
         "CustomerID", -1)
      customer.TerritoryID = customer.SafeRead(reader, _
         "TerritoryID", -1)
      customer.CustomerType = customer.SafeRead(reader, _
         "CustomerType", "S")
      customer.RowGuid = customer.SafeRead(reader, "rowguid", _
         Guid.NewGuid())
      customer.ModifiedDate = customer.SafeRead(reader, _
         "ModifiedDate", DateTime.Now)

      Return customer
   End Function

   <Extension()> _
   Function SafeRead(Of T)(ByVal customer As Customer, _
      ByVal reader As SqlDataReader, ByVal fieldName As String, _
      ByVal defaultValue As T) As T

      Try
         Return Convert.ChangeType(reader(fieldName), _
            defaultValue.GetType())
      Catch ex As Exception
         Return defaultValue
      End Try

   End Function

   Public Class Customer
      Private _customerID AsInteger
      Public Property CustomerID() As Integer
         Get
            Return _customerID
         End Get
         Set(ByVal Value As Integer)
            _customerID = Value
         End Set
      End Property

      Private _territoryID As Integer
      Public Property TerritoryID() As Integer
         Get
            Return _territoryID
         End Get
         Set(ByVal Value As Integer)
            _territoryID = Value
         End Set
      End Property

      Private _accountNumber As String
      Public Property AccountNumber() As String
         Get
            Return _accountNumber
         End Get
         Set(ByVal Value As String)
            _accountNumber = Value
         EndSet
      End Property

      Private _customerType As String
      Public Property CustomerType() As String
         Get
            Return _customerType
         End Get
         Set(ByVal Value As String)
            _customerType = Value
         End Set
      End Property

      Private _rowGuid As Guid
      Public Property RowGuid() As Guid
         Get
            Return _rowGuid
         End Get
         Set(ByVal Value As Guid)
            _rowGuid = Value
         End Set
      End Property

      Private _modifiedDate As DateTime
      Public Property ModifiedDate() As DateTime
         Get
            Return _modifiedDate
         End Get
         Set(ByVal Value As DateTime)
            _modifiedDate = Value
         End Set
      End Property

   End Class

End Module

If you need a refresher on extension methods, LINQ, or any of the techniques in this article, you can check out many years of back articles on codeguru.com (in the VB Today column) or developer.com.

Summary

Functional construction basically is a part of LINQ to XML that lets you construct an XML document from any data with one line of code. Because XML is self-describing and text, it is highly portable and highly desirable as a standard technology. Being able to easily move to and from an XML format is also highly desirable.

About the Author

Paul Kimmel is the VB Today columnist for www.codeguru.com and has written several books on object-oriented programming and .NET. Check out his upcoming book LINQ Unleashed for C# due in July 2008. Paul Kimmel is an Application Architect for EDS. You may contact him for technology questions at pkimmel@softconcepts.com.

Lansing is having a free Day of .NET training at Lansing Community College on June 21st. Check out the web site for details. The group likes to think of it as a poor man's TechEd (because it's free), but the content will be excellent.

Copyright © 2008 by Paul T. Kimmel. All Rights Reserved.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • You may already know about some of the benefits of Bluemix, IBM's open platform for developing and deploying mobile and web applications. Check out this webcast that focuses on building an Android application using the MobileData service, with a walk-through of the real process and workflow used to build and link the MobileData service within your application. Join IBM's subject matter experts as they show you the way to build a base application that will jumpstart you into building your own more complex app …

  • On-demand Event Event Date: September 17, 2014 Another day, another end-of-support deadline. You've heard enough about the hazards of not migrating to Windows Server 2008 or 2012. What you may not know is that there's plenty in it for you and your business, like increased automation and performance, time-saving technical features, and a lower total cost of ownership. Check out this webcast and join Rich Holmes, Pomeroy's practice director of virtualization, as he discusses the future state of your servers, …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds