ADO.Net DataSets, Web Services and Interoperability
Just the other day I overheard my friend Amigo having a little technical discussion with his colleagues. His colleagues hang together and sing on similar tunes in great harmony. Therefore, for the purpose of this post I will refer to them as a single entity: ‘peers’. The subject of discussion was the same as the subject of this post. Here’s how the discussion went:
Peers: Hey Amigo, why have you coded the Web Services layer the way you have?
Amigo: What do you mean? Is there something wrong with that?
Peers: Yes. Why don’t you return DataSets from your Web Methods?
Amigo: Why?
Peers: Don’t you know that they are so functionally rich? They give you a perfect relational view in memory. They have awesome FindBy, GetChildRows style methods. You almost have to do nothing in order to manipulate them.
Amigo: But, they are not very ‘interoperable’!??
Peers: Laughs, giggles, chuckles (in unison)
Amigo: (With great tenacity) And they produce a lot larger payload size.
Peers: More laughs, giggles, chuckles (in unison)
That’s when Amigo agreed to disagree, gave up and went back into his box. After all, Amigo does not care that much.
However, I do. So I have decided put together an example on this blog to explain what Amigo was referring to a little more. Read on if you care…
Here is a sample Web Service that uses DataSets as data transport objects. The web service operates on the CustomerDataSet, which contains the following two tables:
Customer and Address. A Customer can have multiple addresses. Here’s how the GetCustomer() Web Method looks like:
1: [WebService(Namespace = "http://noname.org/schemas/Customer")]
2: [ToolboxItem(false)]
3: public class CustomerService : System.Web.Services.WebService
4: {
5:
6: [WebMethod]
7: public CustomerDataSet GetCustomer(int customerId)
8: {
9: return FindCustomer(customerId);
10: }
11:
12: private CustomerDataSet FindCustomer(int customerId)
13: {
14: CustomerDataSet dataset = new CustomerDataSet();
15: dataset.ReadXml(Server.MapPath(@"~/Customer.xml"));
16: return dataset;
17: }
18: }
Yes. It is a rather simplistic Web Method that returns a DataSet that contains three records.
The GetCustomer() Web Method returns the following SOAP response:
1: ResponseCode: 200 (OK)
2: Server:Microsoft-IIS/5.1
3: Date:Thu, 11 Sep 2008 03:46:57 GMT
4: X-Powered-By:ASP.NET
5: X-AspNet-Version:2.0.50727
6: Cache-Control:private, max-age=0
7: Content-Type:text/xml; charset=utf-8
8: Content-Length:3172
9:
10: <?xml version="1.0" encoding="utf-16"?>
11: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
12: <soap:Body>
13: <GetCustomerResponse xmlns="http://noname.org/schemas/Customer">
14: <GetCustomerResult>
15: <xs:schema id="CustomerDataSet" targetNamespace="http://tempuri.org/CustomerDataSet.xsd" xmlns:mstns="http://tempuri.org/CustomerDataSet.xsd" xmlns="http://tempuri.org/CustomerDataSet.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" attributeFormDefault="qualified" elementFormDefault="qualified">
16: <xs:element name="CustomerDataSet" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
17: <xs:complexType>
18: <xs:choice minOccurs="0" maxOccurs="unbounded">
19: <xs:element name="Customer">
20: <xs:complexType>
21: <xs:sequence>
22: <xs:element name="CustomerId" type="xs:int" />
23: <xs:element name="FirstName" type="xs:string" minOccurs="0" />
24: <xs:element name="LastName" type="xs:string" minOccurs="0" />
25: <xs:element name="Address" minOccurs="0" maxOccurs="unbounded">
26: <xs:annotation>
27: <xs:appinfo>
28: <msdata:Relationship name="Customer_Address" msdata:parent="Customer" msdata:child="Address" msdata:parentkey="CustomerId" msdata:childkey="CustomerId" />
29: </xs:appinfo>
30: </xs:annotation>
31: <xs:complexType>
32: <xs:sequence>
33: <xs:element name="AddressId" type="xs:int" />
34: <xs:element name="CustomerId" type="xs:int" minOccurs="0" />
35: <xs:element name="Line1" type="xs:string" minOccurs="0" />
36: <xs:element name="Line2" type="xs:string" minOccurs="0" />
37: <xs:element name="Suburb" type="xs:string" minOccurs="0" />
38: <xs:element name="State" type="xs:string" minOccurs="0" />
39: <xs:element name="PostCode" type="xs:string" minOccurs="0" />
40: </xs:sequence>
41: </xs:complexType>
42: </xs:element>
43: </xs:sequence>
44: </xs:complexType>
45: </xs:element>
46: </xs:choice>
47: </xs:complexType>
48: <xs:unique name="Address_Constraint1" msdata:ConstraintName="Constraint1" msdata:PrimaryKey="true">
49: <xs:selector xpath=".//mstns:Address" />
50: <xs:field xpath="mstns:AddressId" />
51: </xs:unique>
52: <xs:unique name="Constraint1" msdata:PrimaryKey="true">
53: <xs:selector xpath=".//mstns:Customer" />
54: <xs:field xpath="mstns:CustomerId" />
55: </xs:unique>
56: </xs:element>
57: </xs:schema>
58: <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
59: <CustomerDataSet xmlns="http://tempuri.org/CustomerDataSet.xsd">
60: <Customer diffgr:id="Customer1" msdata:rowOrder="0" diffgr:hasChanges="inserted">
61: <CustomerId>1</CustomerId>
62: <FirstName>John</FirstName>
63: <LastName>Smith</LastName>
64: <Address diffgr:id="Address1" msdata:rowOrder="0" diffgr:hasChanges="inserted">
65: <AddressId>1</AddressId>
66: <CustomerId>1</CustomerId>
67: <Line1>40</Line1>
68: <Line2>Kent Street</Line2>
69: <Suburb>Sydney</Suburb>
70: <State>NSW</State>
71: <PostCode>2000</PostCode>
72: </Address>
73: <Address diffgr:id="Address2" msdata:rowOrder="1" diffgr:hasChanges="inserted">
74: <AddressId>2</AddressId>
75: <CustomerId>1</CustomerId>
76: <Line1>140</Line1>
77: <Line2>George Street</Line2>
78: <Suburb>Brisbane</Suburb>
79: <State>QLD</State>
80: <PostCode>7000</PostCode>
81: </Address>
82: </Customer>
83: </CustomerDataSet>
84: </diffgr:diffgram>
85: </GetCustomerResult>
86: </GetCustomerResponse>
87: </soap:Body>
88: </soap:Envelope>
A whopping eighty lines of SOAP payload to return three records! Damn! That’s Amigo’s problem number one. The problem of size. The response message contains the DataSet schema as well as data. It is like sending the class definition as well as the class object over the wire. Note the content within the <xs:schema> tag.
Amigo’s second problem is that of interoperability. The non-ms.net world (which is sizeable, and commands respect) does not have much idea about the semantics of an ADO.Net typed DataSet and how to reconstitute this proprietary XML into a useful binary object. Note the following content:
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
msdata:IsDataSet="true" msdata:UseCurrentLocale="true"
diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
Well.. Now let’s look at a sample web service that returns XML Serializable, nested business objects. Here’s the class diagram depicting these business objects:
Here’s how the GetCustomerBO() Web Method looks like:
1: [WebMethod]
2: public BusinessObjects.Customer GetCustomerBO(int customerId)
3: {
4: return FindCustomerBO(customerId);
5: }
6:
7: private BusinessObjects.Customer FindCustomerBO(int customerId)
8: {
9: XmlDocument doc = new XmlDocument();
10:
11: doc.Load(Server.MapPath(@"~/CustomerBO.xml"));
12:
13: BusinessObjects.Customer customer = new CustomerWebService.BusinessObjects.Customer();
14:
15: XmlSerializer serialiser = new XmlSerializer(customer.GetType(), "http://tempuri.org/Customer.xsd");
16:
17: MemoryStream memoryStream = new MemoryStream(this.StringToUTF8ByteArray(doc.InnerXml));
18:
19: XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);
20:
21: customer = (BusinessObjects.Customer)serialiser.Deserialize(memoryStream);
22:
23: return customer;
24: }
The GetCustomerBO Web Method returns the following SOAP response:
1: ResponseCode: 200 (OK)
2: Server:Microsoft-IIS/5.1
3: Date:Thu, 11 Sep 2008 01:30:45 GMT
4: X-Powered-By:ASP.NET
5: X-AspNet-Version:2.0.50727
6: Cache-Control:private, max-age=0
7: Content-Type:text/xml; charset=utf-8
8: Content-Length:807
9:
10: <?xml version="1.0" encoding="utf-16"?>
11: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
12: <soap:Body>
13: <GetCustomerBOResponse xmlns="http://noname.org/schemas/Customer">
14: <GetCustomerBOResult xmlns="http://tempuri.org/Customer.xsd">
15: <CustomerId>1</CustomerId>
16: <FirstName>John</FirstName>
17: <LastName>Smith</LastName>
18: <Address>
19: <AddressId>1</AddressId>
20: <Line1>40</Line1>
21: <Line2>Kent Street</Line2>
22: <Suburb>Sydney</Suburb>
23: <State>NSW</State>
24: <PostCode>2000</PostCode>
25: </Address>
26: <Address>
27: <AddressId>2</AddressId>
28: <Line1>140</Line1>
29: <Line2>George Street</Line2>
30: <Suburb>Brisbane</Suburb>
31: <State>QLD</State>
32: <PostCode>7000</PostCode>
33: </Address>
34: </GetCustomerBOResult>
35: </GetCustomerBOResponse>
36: </soap:Body>
37: </soap:Envelope>
Viola! Down to thirty line payload. Note the response XML. It only contains information that is absolutely necessary. No proprietary garble. No schemas. Hence, making the Web Service interface concise, self-descriptive and interoperable.
Footnote
While my friend Amigo was right. He did not fight the battle. It was hardly worth fighting.
ADO.Net DataSets are awesome. However, they cannot solve all problems. Especially, the integration problems.
Filed under: Technology