Recent reading

Cool articles ! You must read them.

Friday, February 24, 2006

Working with Complex Data Types in an XML Web Service

XML Web Services enable the exchange of complex data types, serialized as XML. Complex data types, such as ADO.NET DataSets and custom classes can be serialized as XML and either sent to the XML Web Service as an input argument, or returned from the XML Web Service as the result. In this article we will build the beginning of an XML Web Service, which we will finish in next week's article, when we will also build a consumer Web application. The XML Web Service will have WebMethods for returning a DataSet and custom classes serialized as XML. For the DataSet, the .NET Framework will handle the formatting of the XML document that represents the DataSet; for the custom classes we will use classes from the System.Xml.Serialization namespace to define the XML format. The information page for the XML Web Service

Working with DataSets

A DataSet can be used with an XML Web Service, and the .NET Framework will automatically handle serializing it as XML. Listing 4.1 shows a serialized DataSet (DataSetName="Northwind") that has one DataTable (TableName="Categories"). The DataTable has two columns, CategoryID (int) and CategoryName (string). The DataTable lists all of the categories in the Northwind Categories table.

Listing 4.1





















1
Beverages


2
Condiments


3
Confections


4
Dairy Products


5
Grains/Cereals


6
Meat/Poultry


7
Produce


8
Seafood



In Listing 4.1 we see how the .NET Framework serializes a DataSet when it is used with an XML Web Service. We can see that the DataSet uses the XML namespace (xmlns) http://www.dotnetjunkies.com. This is used to help uniquely identify this DataSet - the XML namespace is defined in the XML Web Service. When the DataSet is serialized, the XML namespace of the XML Web Service is assigned to the DataSet.

In the XML Schema in Listing 4.1 the DataTable is defined and each column is defined, with its data type. Each record in the DataTable is serialized to a element, with child elements for each of the columns.

To generate the XML document shown in Listing 4.1, we create an XML Web Service that returns the defined DataSet. This is shown in Listing 4.2.

Listing 4.2
[ProductServices.asmx]
<%@ WebService Class="XMLWebServices.C04.ProductServices" %>

[ProductServices.asmx.cs]
using System;
using System.Data;
using System.Web.Services;

namespace XMLWebServices.C04
{
[WebService( Namespace="http://www.dotnetjunkies.com" )]
public class ProductServices : WebService
{
[WebMethod(Description="Returns a DataSet with one DataTable (Categories) of all Categories and CategoryIDs in the Northwind Categories table.")]
public DataSet GetCategories()
{
XMLWebServices.Data.NorthwindDB _nwdDB = new XMLWebServices.Data.NorthwindDB();
return _nwdDB.getCategories();
}
}
}

In Listing 4.2 we create the beginning of the ProductServices XML Web Service. In the [WebService()] attribute we define the namespace for the XML Web Service. This namespace is used as the xmlns attribute of the DataSet when it is serialized as XML.

So far we only have one WebMethod, the GetCategories() method. This method returns the DataSet shown in Listing 4.1. Invoking a method on the NorthwindDB data access class does this. The NorthwindDB class is shown in Listing 4.3.

Listing 4.3
[NorthwindDB.cs]
using System;
using System.Data;
using System.Data.SqlClient;
using System.Text;
using System.Configuration;

namespace XMLWebServices.Data
{
///
/// Data Access Layer for Northwind Database
///
public class NorthwindDB
{
public NorthwindDB(){}

// Create a class-level private connection string object
private String _conString = ConfigurationSettings.AppSettings["NWDConString"];

public DataSet getCategories()
{
// Build the SQL statement
StringBuilder _sql = new StringBuilder();
_sql.Append ( "SELECT CategoryID, CategoryName " );
_sql.Append ( "FROM Categories " );
_sql.Append ( "ORDER BY CategoryName" );
// Create the DataSet to return
DataSet _ds = new DataSet ( "Northwind" );
// Create a Data Adapter to fill the DataSet
SqlDataAdapter _sda = new SqlDataAdapter ( _sql.ToString(), _conString );
// Fill the DataSet and Return it
_sda.Fill ( _ds, "Categories" );
return _ds;
}
}
}

In the NorthwindDB data access class we expose a method, getCategories(), which connects to our SQL Server database and returns a DataSet with one DataTable populated with the CategoryID and CategoryName of all the records in the Categories table in the Northwind database.

The DataSet is passed back to the calling component, the ProductServices class, in the GetCategories() WebMethod, which in turn passes it back to the XML Web Service consumer, serialized as XML. We didn't have to do anything to serialize the DataSet as XML - the .NET Framework does that for us.

Unfortunately, there is no way for us to override how the DataSet is serialized. To create custom XML output we can use the System.Xml.Serialization namespace, and serialize custom classes.

Serializing Custom Classes

We can create a custom class to represent our data, and serialize it as XML to be returned to the XML Web Service consumer. We do this using attribute classes from the System.Xml.Serialization namespace. Some of the XML attribute classes are:

* XmlElementAttribute - Indicates that a public field or property represents an XML element - when the XmlSerializer serializes or deserializes the containing object.
* XmlAttributeAttribute - Specifies that the XmlSerializer should serialize the class member as an XML attribute.
* XmlTextAttribute - Indicates to the XmlSerializer that the member should be treated as XML text when the containing class is serialized or deserialized.
* XmlArrayAttribute - Specifies that the XmlSerializer should serialize a particular class member as an array of XML elements.

Descriptions of all of the XML attribute classes can be found in the .NET Framework SDK documentation at
ms-help://MS.NETFrameworkSDK/cpref/html/frlrfSystemXmlSerialization.htm

The XML attribute classes are used to control how the XmlSerializer serializes and deserializes an object. We can apply these classes to a public field or property, and that informs the XmlSerializer what to do with the object's member.

Using the XML Attribute Classes

We can create a custom class to represent our data - for example a Product class to represent a single product - and use the XML attribute classes to define how a class instance should be serialized as XML.

Lets create a Product class. We will use this in an XML Web Service that will return an instance of the Product class. We will define the XML output to look like the XML document shown in Listing 4.4.

Listing 4.4

xmlns="http://www.dotnetjunkies.com">
ChaiProductName>

39InStock>
0OnOrder>
10ReorderLevel>
ProductStockInformation>
Product>

In Listing 4.4 is the XML representation of a product from the Northwind database. The parent element has two child elements, and .

In the element there is an ID attribute that holds the ProductID value from the Products table - the ProductName value is the text value of the element.

The element has two attributes, Cost and QuantityPerUnit, which are UnitCost and QuantityPerUnit from the Products table, respectively. The element has three child elements, (UnitsInStock), (UnitsOnOrder), and (ReorderLevel).

Listing 4.5 shows the code for the Product, ProductInfo, and StockInfo classes.

Listing 4.5
[Product.cs]
using System;
using System.Data;
using System.Data.SqlClient;
using System.Xml.Serialization;

namespace XMLWebServices.C04
{
///
/// Represents a single product from the Northwind Database
///
public class Product
{
[XmlElement(ElementName="ProductName")]
public ProductInfo ProductInformation = new ProductInfo();
[XmlElement(ElementName="ProductStockInformation")]
public StockInfo ProductStockInformation = new StockInfo();
}

public class ProductInfo
{
[XmlAttribute(DataType="int", AttributeName="ID")]
public Int32 ProductID;
[XmlText()]
public String ProductName;
}

public class StockInfo
{
[XmlElement(DataType="short", ElementName="InStock")]
public Int16 UnitsInStock;
[XmlAttribute(DataType="decimal", AttributeName="Cost")]
public Decimal UnitPrice;
[XmlAttribute(DataType="string", AttributeName="QuantityPerUnit")]
public String QuantityPerUnit;
[XmlElement(DataType="short", ElementName="OnOrder")]
public Int16 UnitsOnOrder;
[XmlElement(DataType="short", ElementName="ReorderLevel")]
public Int16 ReorderLevel;
}
}

In Listing 4.5 we create the Product class. The Product class has two public fields, ProductInformation - which uses the ProductInfo data type - and ProductStockInformation - which uses the StockInfo data type.

Each of the Product public fields have the [XmlElement()] modifier applied to them (the Attribute portion of the XmlWhateverAttribute name is optional). This indicates to the XmlSerializer that these fields should be serialized as XML elements. Since each field uses a custom data type, we can go on to define how the public fields and properties in those classes are serialized. With each [XmlElement()] modifier we define what the name of the element should be when it is serialized. The ProductInformation field will be serialized as ; the ProductStockInformation field will be serialized using the same name as the field. In the case of the ProductStockInformation field, we do not have to define the Name property of the [XmlElement()] modifier, but I did so to be very explicit.

In the ProductInfo class we define two public fields, ProductID (Int32) and ProductName (String). We apply the [XmlAttribute()] class to the ProductID field, indicting this should be serialized as an attribute of the element, using the name "ID" and it should be serialized as an int XML data type. The ProductName field uses the [XmlText()] modifier. This indicates that the value of this field should be serialized as the text of the element.

Based on the settings of the Product.ProductInformation field, and the ProductInfo class, we have defined the following XML elements:

ChaiProductName>
Product>

We use the [XmlElement()] and [XmlAttribute()] modifiers in the same manner to create the XML output for the StockInfo class. The resulting XML is:


39InStock>
0OnOrder>
10ReorderLevel>
ProductStockInformation>
Product>

We can create a WebMethod to return a Product class instance - this is shown in Listing 4.6 (this should be added to the ProductServices XML Web Service).

Listing 4.6
[WebMethod(Description="Returns a custom class, Product, which represents a single Product from the Northwind Products table.")]
public Product GetProductByID ( Int32 ProductID )
{
XMLWebServices.Data.NorthwindDB _nwdDB = new XMLWebServices.Data.NorthwindDB();
return _nwdDB.getProductByID ( ProductID );
}

The GetProductByID() WebMethod takes in a ProductID and returns an instance of the Product class that represents the specified product. The Product class instance is created in the NortwindDB data access component, as shown in Listing 4.7 (this method should be added to the NorthwindDB class).

Listing 4.7
public XMLWebServices.C04.Product getProductByID ( Int32 _productID )
{
// Build the SQL statement
StringBuilder _sql = new StringBuilder();
_sql.Append ( "SELECT ProductName, UnitsInStock, UnitPrice, " );
_sql.Append ( "QuantityPerUnit, UnitsOnOrder, ReorderLevel " );
_sql.Append ( "FROM Products " );
_sql.Append ( "WHERE ProductID=" );
_sql.Append ( _productID.ToString() );
// Create the SqlConnection and SqlCommand objects
SqlConnection _con = new SqlConnection ( _conString );
SqlCommand _cmd = new SqlCommand ( _sql.ToString(), _con );
// Create a Product object
XMLWebServices.C04.Product _product = new XMLWebServices.C04.Product();
// Open the connection and return the IDataReader object
try
{
_con.Open();
// Use a data reader to return the result set
SqlDataReader _rdr = _cmd.ExecuteReader();
// Open the reader to the first record
_rdr.Read();
// Assign the product properties
_product.ProductInformation.ProductID = _productID;
_product.ProductInformation.ProductName = _rdr.GetString(0);
_product.ProductStockInformation.UnitsInStock = (Int16)_rdr.GetValue(1);
_product.ProductStockInformation.UnitPrice = (Decimal)_rdr.GetValue(2);
_product.ProductStockInformation.QuantityPerUnit = _rdr.GetString(3);
_product.ProductStockInformation.UnitsOnOrder = (Int16)_rdr.GetValue(4);
_product.ProductStockInformation.ReorderLevel = (Int16)_rdr.GetValue(5);
// Close the data reader and the connection
_rdr.Close();
_con.Close();
//Return the product object
return _product;
}
catch
{
// If there is an error, return null
return null;
}
}

In Listing 4.7 we create the getProductByID() method of the data access class. This method takes in a ProductID, which it uses to retrieve a record from the Northwind database, Products table. We create an instance of the Product class and set its public field values using the column values in the returned record. The result is a Product class instance that we return to the calling object - the ProductServices XML Web Service's GetProductByID() method - which in turn returns it to the consumer, serialized as XML.

Summary

In next weeks article I will show you how to use the XmlArrayAtribute class by creating a Category class and exposing a property that is an array of Product objects. We will also build a consumer application for the ProductServices XML Web Service and look at how we can consume the XML serialized object that we have created.