Main Menu
Knowledge Base
Product Registration
Log an Incident
Request a Feature
Search Incidents/Bug Reports


Search KB

Please note: In an effort to better serve you, we are in the process of restructuring DevCenter. In the process, we have moved many items that you may be used to finding in DevCenter over to the Main Site. If you are having trouble locating something, please try looking at the following places:

Mapping SQL Data to Class Properties
View Printer Friendly Version

Dino Esposito
Windows Developer Network
Page Options
Average Rating:
5 out of 10

Rate this page
Print this page
E-mail this page
Add to Favorites

Posted:

Sunday, January 04, 2004

Applies To:


  • ADO.NET
  • .NET Framework
  • SQL Server
  • XML

Summary:

Map any XML document stored in a database to any .NET class

As discussed in last month's column, the XML serialization process generates an XML representation of the data stored in a .NET Framework class. Only the public fields of the class are taken into account and the whole process aborts if the class holds circular references with other managed classes. The XmlSerializer class is the .NET Framework tool that governs the process. You use this class to serialize a living instance of a class to a persistence medium and to recreate an object from a source.

Last month, I briefly hinted at a bunch of events. You can take advantage of them during the deserialization step whenever the input stream contains an XML document that doesn't match the schema of the object being deserialized. The programmer can fix things up and programmatically map the unmatched XML node to a particular combination of fields in the target object using events such as UnknownNode. This technique is often used to deserialize across different versions of the class. For example, Version 1.0 of the application saves a class. Next, Version 2.0 of the application that manages a slightly different version of the class needs to deserialize the bytes. Using this approach, the same data can be easily mapped to new, or simply renamed, fields.

Taken to the limit, the feature also proves useful in a more enticing scenario — mapping SQL Server data directly to class instances.

In this article, I'll show how to execute a query that returns XML data and map the various nodes to fields of a predefined class. The process should not be read simply as the deserialization of an instance of the same class. More exactly, the technique discussed here represents a way to map, in total or in part, any XML document stored in a database to any class usable within a .NET application.

Getting XML Data From the Database


Let's prepare a simple query to run against SQL Server that returns XML data. In the latest version, SQL Server supports the FOR XML clause in the SQL's SELECT statement. When used, the clause causes the query processor to pack the result set into an XML document. It goes without saying that any query, and any other database server, can be used as long as it returns text that can be processed as well-formed XML text, or at least as a well-formed XML fragment. (Incidentally, an XML fragment is a well-formed XML document except that it doesn't include a unique root node.)

The following code is at the heart of the example. You call into a method of a worker class and the method executes a SQL XML command. The data flows into the serializer, and an instance of a particular class is returned, as shown here:

Employee emp = LoadEmployeeData(empID);


Internally, the LoadEmployeeData method utilizes the ExecuteXmlReader method of ADO.NET's SqlCommand class to execute the query and obtain back XML data. In the .NET Framework, XML data is normally worked using a reader that is a cursor-like component, which processes one node at a time. The peculiarity of the ExecuteXmlReader method is that it returns the XML data from the query stuffed in a ready-to-use instance of the XML reader class.

// cmd here is a SqlCommand object
XmlSerializer ser = PrepareSerializer();
Employee emp = null;
XmlTextReader reader;
reader = (XmlTextReader) cmd.ExecuteXmlReader();
if(ser.CanDeserialize(reader))
     emp = (Employee) ser.Deserialize(reader);
else
     Console.WriteLine("Cannot deserialize");
reader.Close();


The XML reader is passed to the Deserialize method of the XmlSerializer and its content is processed and creates an instance of the specified user class. For the whole mechanism to work, some attributes must be set on the serializer class. These attributes play an essential role to instruct the serializer on how to map nodes in the source XML to fields in the target class. The configuration attributes are defined in the PrepareSerializer method.

In general, the attributes of the XML serialization process can be set in two ways — programmatically through an ad hoc set of classes, and declaratively by associating attributes with the serializable members of the class. For example, the declaration to follow states that the LastName property must be rendered to XML as a nullable element named FamilyName.

[XmlElement(Namespace ="urn:dino-e",
  IsNullable=true,
  DataType="string",
  ElementName="FamilyName")]
public string LastName;


The same association can be set programmatically by manipulating the corresponding attribute classes such as XmlElementAttribute.

A special case of XML serialization is when the data to deserialize is stored in a database. In a similar situation, the document being returned is not necessarily the result of a previous XML serialization that was operated on in the same output class. In the aforementioned code snippet, you deserialize the output of a query to an Employee class. However, nothing would guarantee that the contents of the database column is exactly the XML string generated by the XmlSerializer for an instance of Employee. At the end of the day, XML serialization attributes allow you to deserialize any XML text, no matter the storage medium, to any .NET Framework class. More importantly, the mapping takes place automatically within the XmlSerializer class and doesn't even require that you create an instance of target class.

Deserializing From a Database


Let's examine the details of XML data mapping considering the following class, named Employee.

public class Employee {
 public string FirstName;
 public string LastName;
 public string Position;
 public DateTime Hired;
}


You run a SQL query against the Northwind database and make sure you select the fields listed in the following statement:

SELECT firstname, lastname, title, hiredate
FROM employees
WHERE <A href="mailto:employeeid=@empID">employeeid=@empID</A>
FOR XML


Since the SELECT statement contains a FOR XML clause, the final XML output will be made of a sequence of XML nodes like the one shown here. You have one of such a node for each record in the result set.

<employees firstname="..."
   lastname="..."
   title="..."
   hiredate="..." />


As mentioned, the serializer is used only to deserialize the data coming from SQL Server. No previous serialization was explicitly done. The deserializer reads the inbound data and infers an ad hoc class structure; then, it just matches this inferred structure with the specified type to deserialize to — the Employees class in this case.

The first piece of information you need to pass on to the XML serializer is the name of the class to deserialize to. By default, the serializer assumes that the name of the target class is the root of the XML fragment. In this case, it will be Employees. To change this to Employee, you must define an XmlRoot attribute on the Employee class. This can be done either declaratively or programmatically. You could do it declaratively as shown in the code snippet here:

[XmlRoot(ElementName=employees)]
public class Employee {
  :
}


If for some reason you don't have access to the source code of the Employee class, then you can proceed programmatically by creating an instance of the XmlRootAttribute class. When you bind attributes programmatically, you must first create an instance of the XmlAttributes class, which represents the whole set of attributes for the XML serializer.

XmlAttributes changes = new XmlAttributes();
XmlRootAttribute newRoot = new XmlRootAttribute();
newRoot.ElementName = "employees";
changes.XmlRoot = newRoot;


The XmlRoot property in XmlAttributes is set with an instance of the XmlAttributeRoot class that was previously configured to represent the requested mapping. However, for the whole thing to become effective, the changes must be added to an XmlAttributeOverrides object, which will then be passed to the type-specific serializer's constructor.

XmlAttributeOverrides over = new XmlAttributeOverrides();
over.Add(typeof(Employee), changes);
XmlSerializer ser = new XmlSerializer(typeof(Employee), over);


In the original XML source, you have four fields — lastname, firstname, title, and hiredate. The last two have a counterpart in the Employee class with a different name — Position and Hired. The deserializer, though, works in a strictly case-sensitive fashion and considers firstname completely different from FirstName. For this reason, renaming the lastname and firstname properties is absolutely necessary.

XmlAttributes changes = new XmlAttributes();
XmlAttributeAttribute fname = new XmlAttributeAttribute();
fname.AttributeName = "firstname";
changes.XmlAttribute = fname;
over.Add(typeof(Employee), "FirstName", changes);


You should note that a distinct XmlAttributes object is required for each element you want to override. The XmlAttributes object collects all the overrides you want to enter on a given element. In this case, after creating a new XmlAttributeAttribute object, you change the attribute name to that of the source SQL field and store the resultant object in the XmlAttribute property of the overriding container. A nearly identical piece of code is needed for the other properties of the Employee class.

It is worth noticing that the root of the XML source must coincide with the name of the class. If this is not the case, then the Deserialize method just fails. If you need to apply some logic during the conversion phase, you can do it using one of the techniques that I discussed in last month's column. In particular, you might want to hook up one of the deserializer's events to be notified of any unknown nodes found. Next, you extract any information from nodes and process it as needed.

Working with Embedded Data


What happens if the query contains embedded data; for example, the results of an INNER JOIN? In this case, the FOR XML clause comes up with data packed as shown here:

<employees>
<firstname>Nancy</firstname>
<lastname>Davolio</lastname>
   <orders>
      <orderid>1234</orderid>
      <orderid>5678</orderid>
   </orders>
</employees>


How can you import and map the embedded data rooted in the <orders> node? The XML serializer class is always notified of any <orders> elements found along the way through an UnknownElement event. So to import the contents of the <orders> subtree you need to write an event handler. For example, suppose that you want all the orders to populate a collection member (say, Orders) of the Employee class. The following code illustrates how to handle the event.

if (e.Element.Name == "orders")
{
 if (emp.Orders == null)
         emp.Orders = new ArrayList();
 int oID = (int) e.Element.InnerText;
 emp.Orders.Add(oID);
}


If the collection is NULL, it is created and added to all the various orders. The specific order information is retrieved using the InnerText property of the element. The necessary code is slightly different if the order is expressed as an attribute, as shown here:

<orders orderid="1234" />


In this case, you rely on the Attributes property of the e.Element object:

int oID = (int) e.Element.Attributes["orderid"].Value;
emp.Orders.Add(oID);


The Attributes collection property contains all the attributes on the current node.

<SUbHeader>More About the XmlSerializer Class


When you run a sample application that uses the XmlSerializer class in Visual Studio .NET, you'll notice that the first time the class is initialized in the session, it takes a while. The reason is that the XmlSerializer needs to compile and then store an ad hoc assembly that it uses to perform the serialization and deserialization of the specified type. When you instantiate the XML serializer, you indicate a type. The initialization of the serializer just consists of the creation of an assembly capable of processing data of that type.

The XmlSerializer class maintains an internal table of type/assembly pairs. If there is no known assembly to handle the type, a new assembly is promptly generated and cached; otherwise, the existing assembly is used to serialize and deserialize. A little known thing, though, is that if you happen to use an XmlSerializer constructor different from the simplest one, the assembly cache is disabled and the assembly is recreated each time the class is instantiated.

Type t = typeof(MyClass);
XmlSerializer ser = new XmlSerializer(t);


Of all the constructors available to the serializer, the one just shown is the only one not affected by the aforementioned behavior. If you use another constructor, then be aware of the fact that the internal assembly cache is disabled.

The XML serializer is a double-edged sword. On one hand, it lets you serialize and deserialize even complex .NET Framework classes to and from XML with very few lines of code. To obtain that, though, the serializer creates an assembly on the fly. Unless you use a single global instance of the serializer on a per-type basis, you can easily add hundreds of milliseconds of overhead to each call. Watch your constructor and enjoy serialization! w::d

Dino Esposito is Wintellect's ADO.NET and XML expert and is a trainer and consultant based in Rome, Italy. He is a contributing editor to MSDN Magazine, writing the "Cutting Edge" column, and is the author of several books for Microsoft Press, including Building Web Solutions with ASP.NET and Applied XML Programming for .NET. Contact him at dinoe@wintellect.com.

Article provided by:

How would you rate the quality of this content?
Poor -----------------------------------------> Outstanding

Tell us why you rated the content this way. (optional)