Magazine > Issues > 2006 > June >  Serializing XML With Schema Providers In The .N...
Class To Contract
Enrich Your XML Serialization With Schema Providers In The .NET Framework
Keith Pijanowski

This article discusses:
  • The .NET Framework attribution model
  • Converting between object instances and XML
  • Converting between schemas and classes
  • Customizing XML serialization and deserialization
This article uses the following technologies:
.NET Framework 2.0, XML
Code download available at: SchemaProviders2006_06.exe (191 KB)
Browse the Code Online
M apping classes to schemas and serializing objects to XML documents leads to interoperability. Consider the relationship that exists among classes, schemas, objects, and XML documents (as illustrated in Figure 1). Classes are constructs that describe a software system's data in a proprietary way, as opposed to schemas which allow data to be described using a proven standard. By using a standard, schemas allow for interoperability between systems, platforms, and organizations. When working with classes, you might not consider that the data being described may some day be visible to another system if a Web service is implemented within your system. By mapping classes to schemas, data that was originally meant for internal system use can be set free, traveling from system to system in the form of SOAP messages.
Figure 1 Mapping Schemas and Objects 
Unfortunately, the Microsoft® .NET Framework 1.x provides only minimal options for mapping classes to schemas and serializing objects to XML documents. As a result, when architects design service-oriented architectures with object-oriented technologies, the task of performing this sort of mapping offers quite a challenge. The .NET Framework 2.0, however, addresses the limitations with the introduction of Schema providers and the IXmlSerializable interface. These new tools put the mapping of class to schema and XML serialization/deserialization in the hands of developers and architects.
In this article, I'll show you how to use these tools and explain why they are important. In order to better understand the issues involved in mapping classes to schemas and objects to XML documents, I will review the current capabilities found in the .NET Framework 1.1, focusing on its architectural shortcomings. I'll then discuss Schema providers, elaborating on how they allow developers to programmatically create the XML Schema that will be used to represent a class. And then I will take a look at the added support for XML serialization and deserialization in the .NET Framework 2.0.

The .NET Attribution Model
Using the capabilities of the .NET Framework 1.1, it is possible to let the Framework itself handle most of the work needed to map classes to schemas and objects to XML documents. Both mapping and serialization are accomplished by using the attributes and utilities available in the System.Xml.Serialization namespace. This technique, which I will refer to as the .NET attribution model, provides a declarative programming model that is easy to use. However, it places limits on what can be put in both the schema and the document.
Consider this simple Product class:
namespace SimpleExample 
{
   public class Product 
   {
       public int ProductID;
       public string Name;
       public decimal ListPrice;
   }
}
If this needs to be used as the return type of a Web service or the type of a Web service input parameter, then there would have to be a way to map the .NET Framework type system into the XML Schema type system. The .NET Framework will do this mapping for you automatically if you let it. For example, say the Product class example is packaged into an assembly named BusinessEntities.dll and the Product class is in the SimpleExample namespace. If this is the case, then the schema shown in Figure 2 will be generated if I issue the following command from the Visual Studio command prompt:
xsd BusinessEntities.dll /type:SimpleExample.Product
Note that the xsd.exe command-line tool does not have an output parameter to allow for the specification of an output file. When you execute this command, the generated schema is placed in a file named schema0.xsd, located in the directory in which the command was executed.
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <xs:element name="Product" nillable="true" type="Product" />

    <xs:complexType name="Product">
        <xs:sequence>
            <xs:element minOccurs="1" maxOccurs="1" 
             name="ProductID" type="xs:int" />
            <xs:element minOccurs="0" maxOccurs="1" 
             name=»Name» type=»xs:string» />
            <xs:element minOccurs="1" maxOccurs="1" 
             name="ListPrice" type=»xs:decimal» />
        </xs:sequence>
    </xs:complexType>

</xs:schema>

It is not a good idea to let the .NET Framework generate a schema from a class like the one shown in Figure 2. The problem with this schema is that no target namespace has been defined. The namespaces in XML Schemas serve the same purpose as namespaces in an object-oriented language—they help to ensure unique names for custom types. A schema's target namespace is the namespace in which all complex types defined within that schema will be placed.
Another issue with the schema in Figure 2 is that the root element declaration has the same name as its type: Product. This would be like declaring a variable with the same name as the variable's type, as shown here:
Product Product = new Product();
This would make your code difficult to understand. Likewise, it makes the schema difficult to comprehend.
Fortunately, the creators of the .NET Framework have provided a few tools in the form of .NET attributes to allow you to correct these problems. The following class is an improved version of the one shown previously:
[XmlType("product_type",Namespace = 
    "http://Attribution.Example.org/Product.xsd")]
[XmlRoot(Namespace = "http://Attribution.Example.org/Product.xsd" 
    ElementName = "product_root", IsNullable = false)]
public class Product
{
    [XmlElement("product_id")]
    public int ProductID;
    [XmlElement("name")]
    public string Name;
    [XmlElement("list_price")]
    public decimal ListPrice;
}
It uses the XmlType attribute to specify a target namespace for the complex type that will be used to represent the Product class in the resulting schema. This attribute also allows the developer to assign a type name that is different from the class name. In this example the target namespace is assigned a value of "http://Attribution.Example.org/Product.xsd" by using the Namespace property of the XmlType attribute, and the type name is assigned a value of product_type by using the first parameter of this attribute.
The XmlRoot attribute is used to assign a name to the element declaration that will represent the document root. In the new version of the Product class, this attribute is used to assign product_root as the name of the element that will represent the root in the XML document being described by the schema. This attribute also allows the developer to assign the root element to a namespace. This will typically be the target namespace specified through the XmlType attribute.
Finally, note the use of the XmlElement attribute. This attribute is used to explicitly state that the resulting schema should use element normal form for all the fields. This attribute also allows the developer to specify each field's element name (as opposed to each element being given the same name as the field itself). The schema generated for the new Product class is shown in Figure 3.
<?xml version="1.0" encoding="utf-8"?>
<xs:schema 
  targetNamespace=http://AttributionExample.xsd/Product.xsd
  xmlns:tns="http://AttributionExample.xsd/Product.xsd" 
  elementFormDefault="qualified" 
  xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <xs:element name="product_root" type="tns:product_type" />

    <xs:complexType name="product_type">
       <xs:sequence>
           <xs:element minOccurs="1" maxOccurs="1" 
               name=»product_id» type=»xs:int» />
           <xs:element minOccurs=»0» maxOccurs=»1» 
               name=»name» type=»xs:string» />
           <xs:element minOccurs=»1» maxOccurs=»1» 
               name=»list_price» type=»xs:decimal» />
        </xs:sequence>
    </xs:complexType>

</xs:schema>


Objects and XML Documents
XML serialization (and deserialization for that matter) is relatively painless with the .NET Framework 1.1. There are some disadvantages, though. Let's first look at the simplicity of performing such tasks in the .NET Framework 1.1.
The following code snippet instantiates the Product class, fills it with data, and serializes it:
Product p = new Product();
p.ProductID = 1234;
p.ListPrice = 500.00M;
p.Name = "Mountain Bike";

XmlSerializer ser = new XmlSerializer(p.GetType());

StringWriter s = new StringWriter();
ser.Serialize(s, p);

txtXML.Text = s.ToString();
(Note that this code is part of the download that accompanies this article.) The serialization code creates the XML document that is shown in the following snippet:
<?xml version="1.0" encoding="utf-16"?>
<product_root 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns="http://AttributionExample.xsd/Product.xsd">
    <product_id>1234</product_id>
    <name>Mountain Bike</name>
    <list_price>500.00</list_price>
    <dealer_price>450.00</dealer_price>
</product_root>
Notice that to serialize an object to XML, there is no need for code that is specific to the object's type. The XmlSerializer class uses the information specified through the attributes discussed in the previous section to create an XML document that contains the information found in the object. Specifically, notice that the name of the root element and the names of the elements that represent the object's public fields conform to the names specified through attribution.
Since the .NET Framework generates both the schema and the XML document, you can safely assume that the document can be validated against the schema in Figure 3.
Deserializing an XML document into an object is just as easy as serializing an object to an XML document. The following code snippet will deserialize the previous XML document and produce a Product object:
StringReader sr = new StringReader(txtXML.Text);
Product p = new Product();
XmlSerializer ser = new XmlSerializer(p.GetType());
p = (Product) ser.Deserialize(sr);
From this example it is easy to see that the big advantage to using the .NET attribution model is ease of implementation. By utilizing just three attributes—XmlType, XmlRoot, and XmlElement—a class can be accurately mapped to a schema and the .NET Framework will take care of serialization and deserialization.
What you won't glean from this example, however, are the limitations placed on architects and developers as a result of these easy to use utilities. There are a few subtle annoyances with the attribution model. First, you cannot take full advantage of the power of XML Schemas. It would be nice, for example, to limit the name element to a specified number of characters. This capability is provided by the XML Schema standard, but there is no way to express this restriction using the .NET attribution model. The XML Schema standard also provides options such as pattern matching, numeric ranges, and enumerations that can be used for restricting values of simple data types. These restrictions are either not supported by the .NET attribution model or they are cumbersome to implement using attributes.
Another minor issue with .NET attribution is that you have no way to execute business logic during serialization and deserialization. If a complex object graph is being recreated during deserialization, you may want to temporarily turn off all validation logic during deserialization and then fire a validating event once you know that all objects are fully hydrated.
By far the biggest issue with the .NET attribution model is the fact that the data contract that the outside world sees is tightly coupled with the object-oriented accessibility options of the .NET Framework. Specifically, only read/write public properties and fields can be attributed using the attributes found in the System.Xml.Serialization namespace. When using the .NET attribution model, you cannot expose a private member variable (or any other non-public member) as part of your data contract. Additionally, read-only properties and fields are not supported with this model.

Schema Providers
Schema providers are a new tool for designing the data contracts of Web services. They provide complete control and flexibility for mapping class to schema. A schema provider, which is a static function specified by the XmlSchemaProvider attribute (a class-level attribute), is responsible for setting up a schema for the class in which it resides. Specifying a schema provider causes the default class-to-schema mapping to be overridden and places control directly in the hands of the developer. When a schema provider is specified, tools like xsd.exe will call into this static function when creating schemas or when looking for complex type definitions that will be used to represent the class to the outside world.
Specifying a custom schema necessitates custom serialization and deserialization. Therefore, the .NET Framework 2.0 now provides support for the IXmlSerializable interface.
With the flexibility offered by schema providers, you don't have to rely on .NET attribution. You can design the schema. And then, once you are satisfied with the design of the schema, you can then implement your class.
The schema in Figure 4 represents the redesigned data contract for the Product class. This schema is similar to the schema discussed earlier but has been given a few improvements that demonstrate the flexibility of schema providers. First, the name element is limited to 25 characters. Second, there is now an additional element declaration, dealer_price, to represent the dealer price.
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:tns="http://SchemaProvider.Example.org/Product.xsd" 
    elementFormDefault="qualified" 
    targetNamespace=http://SchemaProvider.Example.org/Product.xsd
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <xs:element name="product_root" type="tns:product_type" />

    <xs:complexType name="product_type">
        <xs:sequence>
            <xs:element name="product_id" type="xs:int" />
            <xs:element name="name" type="tns:NameType" />
            <xs:element name="list_price" type="xs:decimal" />
            <xs:element name="dealer_price" type="xs:decimal" />
        </xs:sequence>
    </xs:complexType>

    <xs:simpleType name="NameType">
        <xs:restriction base="xs:string">
            <xs:maxLength value="25" />
        </xs:restriction>
    </xs:simpleType>

</xs:schema>

Some improvements are also needed in the design of the Product class. ProductID is going to become a read-only property that is mapped to the product_id element. A new field, dealerPrice, is mapped to the new dealer_price element, and it is a private member variable that is not accessible to other objects within the system.
The code in Figure 5 shows a schema provider that builds the schema shown in Figure 4. This code shows the portion of our Product class that declares and implements a schema provider. The schema provider is specified by using the XmlSchemaProvider attribute, which merely specifies the name of the function that should be called when schema information for this class is needed. In this case, the name of the function is GetSchemaFile. As noted earlier, this function must be static because tools like xsd.exe do not want to instantiate the class in order to use the schema provider.
[XmlRoot(ElementName="product_root", DataType="product_type", 
    Namespace="http://SchemaProvider.Example.org/Product.xsd",
    IsNullable = false)]
[XmlSchemaProviderAttribute("GetSchemaFile")]
public class Product : IXmlSerializable
{ 
    public static XmlSchemaComplexType GetSchemaFile(
        System.Xml.Schema.XmlSchemaSet xs)
    {
        string xsdFile = Directory.GetCurrentDirectory() + 
            "\\Product.xsd";
        XmlSerializer schemaSerializer = 
            new XmlSerializer(typeof(XmlSchema));
        XmlSchema schema = 
            (XmlSchema)schemaSerializer.Deserialize(
                XmlReader.Create(xsdFile));
        xs.Add(schema);

        // target namespace
        string tns = „http://SchemaProvider.Example.org/Product.xsd";  
        XmlQualifiedName name = 
            new XmlQualifiedName(„product_type", tns);
        XmlSchemaComplexType productType = 
            (XmlSchemaComplexType) schema.SchemaTypes[name];

        return productType;
    } 

    ...
}

The signature of this static function is interesting and deserves a few comments. Notice that it is expecting to be passed an XmlSchemaSet object and not just an XmlSchema object. With this design, you are free to build up as many schemas as you need, set them into this XmlSchemaSet object, and have them reference each other through namespaces in any fashion. Having a collection of schemas is useful when your class contains properties that are not simple data types, but typed as other classes that have schema providers of their own. For the sake of simplicity, the example I will describe builds up a single schema that is structured similarly to the schema that was created using the .NET attribution model, but which takes advantage of schema capabilities not exposed by the .NET attribution model.
Finally, note that the schema provider in Figure 5 returns an XmlSchemaComplexType object to inform the .NET Framework which complex type represents the class. This is necessary when the schema or schemas contained in the XmlSchemaSet object contain more than one complex type definition.
The GetSchemaFile function illustrates the simplest way to implement a schema provider. This provider reads an external schema file, Product.xsd, which is located in the same directory as the assembly, and streams it into an XmlSchema object. The schema object is then added to the XmlSchemaSet parameter that was passed into the GetSchemaFile function.
Using an external schema file allows you to develop your schema with the editor of your choice. As long as you have solid processes for source code control, source code versioning, and assembly deployment, you will not have a problem.
It is also possible to build a schema programmatically using the classes found in the System.Xml.Schema namespace. This technique allows for the creation of a schema provider that is not dependent on any external resources. However, the classes in this namespace can be tricky to use and you will find yourself writing a lot of code if you have a lot of fields and properties to map to a schema. Nonetheless, if you are interested in investigating this technique, you'll find a purely programmatic schema provider that produces the schema of Figure 4 in the code download for this article, which is available on the MSDN Magazine Web site.

Custom Serialization and Deserialization
Specifying a custom schema implies that custom code is required to correctly produce an XML document that corresponds to the schema coming out of the schema provider. The Product class of Figure 5 implements the IXmlSerializable interface. This interface allows the .NET Framework to call into your object for custom serialization as well as custom deserialization.
The IXmlSerializable interface has three functions: GetSchema, WriteXml, and ReadXml. The GetSchema function should not be used for producing schema information; a schema provider is a better option. For the purposes of backward compatibility, it was decided not to deliver a new version of this interface with the .NET Framework 2.0. The implementation of this function should throw an error in the event that it is erroneously called. The following code snippet is an example of how to throw an error out of the GetSchema function:
XmlSchema IXmlSerializable.GetSchema()
{
    throw new System.NotImplementedException(
        "The method or operation is not implemented.");
}
If you look closely at the signature of IXmlSerializable.GetSchema, you'll see why a schema provider is a better option. IXmlSerializable.GetSchema does not accept an XmlSchemaSet object in case you need to build up a collection of schemas to represent your class. Also, this function does not return an XmlSchemaComplexType object to inform the .NET Framework which complex type represents your class in the case that your schema contains more than one complex type definition.
The following code shows a valid implementation of the IXmlSerializable.WriteXml method for the Product class:
void IXmlSerializable.WriteXml(System.Xml.XmlWriter writer)
{
    string ns = "http://SchemaProvider.Example.org/Product.xsd";
    writer.WriteElementString("product_id", ns, ProductID.ToString());
    writer.WriteElementString("name", ns, Name.ToString());
    writer.WriteElementString("list_price", ns, ListPrice.ToString());
    writer.WriteElementString("dealer_price", ns, 
        dealerPrice.ToString());
}
And here is the XML document that will be produced when a product object is serialized to XML:
<?xml version="1.0" encoding="utf-16"?>
<product_root xmlns="http://SchemaProvider.Example.org/Product.xsd">
    <product_id>1111</product_id>
    <name>Mountain Bike</name>
    <list_price>500.00</list_price>
    <dealer_price>450.00</dealer_price>
</product_root>
The root element of the document will be created for you according to what is specified in the XmlRoot attribute. Hence, you do not have to write this element out yourself using WriteElementStart and WriteElementEnd.
Notice that there are no limitations to what can be serialized. Since this function resides within the Product class, you have access to all the object's resources and any entity may be written to the XmlWriter stream, including read-only members and non-public (private, protected, internal, or internal protected) members.
This function can also contain any business logic that needs to execute during serialization. For example, you can place auditing logic in this function in order to make note of sensitive information leaving the system.
Deserialization is accomplished with the IXmlSerializable.ReadXml method, as shown here:
void IXmlSerializable.ReadXml(System.Xml.XmlReader reader)
{
   string ns = "http://SchemaProvider.Example.org/Product.xsd";
   reader.ReadStartElement();

   ProductID = reader.ReadElementContentAsInt("product_id", ns);
   Name = reader.ReadElementContentAsString("name", ns);
   ListPrice list = reader.ReadElementContentAsDecimal("list_price", ns);
   dealerPrice = reader.ReadElementContentAsDecimal("dealer_price", ns);

   reader.ReadEndElement();
}
There are a few caveats to be aware of when implementing this function. First, this code is responsible for reading the root element. This is in contrast to the serialization process during which the .NET Framework writes out the root element automatically. Second, if any elements being read in are qualified with a namespace, this code must pass in the namespace to the appropriate function of the XmlReader. Failure to specify the correct namespace for namespace qualified elements results in a run-time error because the element reading functions will not be able to find the correct element to read within the XML stream.
The control and flexibility of schema providers and the IXmlSerializable interface come with great responsibility. There is no guarantee that the XML created within IXmlSerializable.WriteXml will validate against the schema generated by the class's schema provider. Furthermore, if you are not careful, the XML written by IXmlSerializable.WriteXml may not be recognizable by IXmlSerializable.ReadXml. It is always a good idea to implement a test harness where you can test your schema provider and your implementation of IXmlSerializable to ensure that data is correctly serialized and deserialized, and that the XML you serialize out of your object can be validated against the schema provided by the schema provider. The code download for this article contains a test harness for the Product class I've described in this article. It is a simple Windows Forms test harness built with Visual Studio 2005. It implements the classes described in this article. This test harness is designed for educational purposes and can be used to view the schemas and XML documents that are generated from classes and objects, respectively. Specifically this test harness can be used to do the following:
  • View the schema that is generated by a schema provider.
  • View the XML document generated by IXmlSerializable.WriteXml.
  • Test serialization and deserialization to make sure that what is serialized by IXmlSerializable.WriteXml can be utilized by IXmlSerializeable.ReadXml to correctly recreate an object from an XmlDocument.
  • Validate the XML generated by IXmlSerializable.WriteXml against the schema that is produced by the schema provider.
This test harness has three groups of command buttons used to compare the .NET attribution model to the new capabilities of the .NET Framework 2.0 (schema providers and the IXmlSerializable interface). The first group of buttons makes use of an attributed version of the Product class described in this article. The second group of command buttons uses a Product class with a schema provider and an implementation of the IXmlSerializable interface. The third group of buttons is an implementation of using schema providers with an object graph or objects that have other objects as their properties. (This was not covered in the article, so check out this implementation if you plan to use schema providers to describe a complex class hierarchy.)
When it comes to ensuring the quality of your classes, the unit testing capabilities of Visual Studio Team Developer and Visual Studio Team Test offer a better option than this test harness. Unit tests can easily be generated to create schemas, serialize objects to XML, deserialize XML into objects, and validate XML against schemas. Such unit tests can be tied to check-in policy and can be set up to automatically run against the builds created with Team Foundation Server.
Figure A Schema Provider Test Harness 
Web Services
Thus far a good amount of time has been spent on the basics of schema providers. Understanding schema providers is essential to understanding the fundamentals of describing data in two different type systems (CLR and XML Schema), as well as understanding the mechanics of moving data from one type system to the other. This section moves beyond the basics to discuss how you can use schema providers in real systems that need to be designed and implemented for interoperability. In other words, I'll show you how to use schema providers to build interoperable Web services.
Consider the Web service shown in Figure 6. This Web service contains a single WebMethod that accepts an integer and returns an instance of the Product class that is equipped with a schema provider. Figure 7 shows the Web Services Description Language (WSDL) for this Web service. The types element is the foundation of a WSDL document. It contains element declarations for all request messages and for all response messages. If additional type definitions are needed to build up a request message or a response message, then both of these type definitions are also included in the types element.
<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions ... Namespace declarations omitted for brevity ... >

  <wsdl:types>
    <s:schema elementFormDefault="qualified"
      targetNamespace="http://YourCompany.com/MyService">
      <s:import namespace=
        "http://SchemaProvider.Example.org/Product.xsd" />
      <s:element name="GetProductByID">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="1" maxOccurs="1" name="id" 
                type="s:int" />
          </s:sequence>
        </s:complexType>
      </s:element>

      <s:element name="GetProductByIDResponse">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1"
                name="GetProductByIDResult" type="s1:product_type" />
          </s:sequence>
        </s:complexType>
      </s:element>

    </s:schema>

    <s:schema xmlns:tns="http://SchemaProvider.Example.org/Product.xsd"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
     elementFormDefault="qualified" targetNamespace=
         "http://SchemaProvider.Example.org/Product.xsd">
      <xs:element name="product_root" type="tns:product_type" />
      <xs:complexType name="product_type">
        <xs:sequence>
          <xs:element name="product_id" type="xs:int" />
          <xs:element name="name" type="tns:NameType" />
          <xs:element name="list_price" type="xs:decimal" />
          <xs:element name="dealer_price" type="xs:decimal" />
        </xs:sequence>
      </xs:complexType>
      <xs:simpleType name="NameType">
        <xs:restriction base="xs:string">
          <xs:maxLength value="25" />
        </xs:restriction>
      </xs:simpleType>
    </s:schema>
    
  </wsdl:types>

  <wsdl:message name="GetProductByIDSoapIn">
    <wsdl:part name="parameters" element="tns:GetProductByID" />
  </wsdl:message>
  <wsdl:message name="GetProductByIDSoapOut">
    <wsdl:part name="parameters" element="tns:GetProductByIDResponse" />
  </wsdl:message>

  <wsdl:portType name="ServiceSoap">
    <wsdl:operation name="GetProductByID">
      <wsdl:input message="tns:GetProductByIDSoapIn" />
      <wsdl:output message="tns:GetProductByIDSoapOut" />
    </wsdl:operation>
  </wsdl:portType>

  <wsdl:binding name="ServiceSoap" type="tns:ServiceSoap">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
    <wsdl:operation name="GetProductByID">
      <soap:operation style="document" soapAction=
        "http://YourCompany.com/MyService/GetProductByID" />
      <wsdl:input>
        <soap:body use="literal" />
      </wsdl:input>
      <wsdl:output>
        <soap:body use="literal" />
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>

  <wsdl:service name="Service">
    <wsdl:port name="ServiceSoap" binding="tns:ServiceSoap">
      <soap:address location=
        "http://localhost:9266/WebServiceTest/Service.asmx" />
    </wsdl:port>
  </wsdl:service>

</wsdl:definitions>

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;

[WebService(Namespace = "http://example.com/MyService")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService
{
    [WebMethod]
    public SchemaProviderExample.Product GetProductByID(int id) 
    {
        // sample data
        decimal list = 500.00M;
        decimal dealer = 450.00M;
        string name = "Mountain Bike";

        return new SchemaProviderExample.Product(id, name, list, dealer);
    }
    
}

Let's walk through of the WSDL document so you can get a concrete example of these concepts. You can see three distinct entries in the types element of the WSDL. The first entry is an element declaration for an element named GetProductByID. This describes the request message for the WebMethod in Figure 6. The GetProductByID element declaration merely states that this message is composed of exactly one integer. The next element declaration is GetProductByIDResponse, which describes the response message of the same method. This states that the response is an instance of the product_type complex type. The final entry in the types element of the WSDL is all the schema information that is returned from the schema provider implemented by the Product class. The complex type named product_type, which is used by the response message, is defined in this final entry.
When this WSDL was generated by wsdl.exe, the schema provider of the Product class was used to define the data contract of the Product class, since an instance of the Product class is the return value of the GetProductByID Web method. When this data contract was placed in the WSDL, it became a part of the message contract for the Web service. This ability to control a message contract with a schema provider represents the value of schema providers.
At run time, the Product class's implementation of IXmlSerializable is used for serialization and deserialization of SOAP messages. In the previous example, the relevant portions of the SOAP response message are created by the Product class's implementation of IXmlSerializable.WriteXml. If an instance of the Product class is needed by a request message (in other words, the Product class is the type of an input parameter) then the implementation of IXmlSerializable.ReadXml is used to deserialize the relevant portions of the SOAP request into an instance of a Product object.
Schema providers are useful when developing Web services in a contract-first fashion. If an application architect is given the WSDL in Figure 7 and asked to build a Web service that conforms to that contract, then the product_type type definition can be copied directly from the WSDL and used to implement a schema provider for an existing Product class or a new Product class. The bottom line is that whether you need to control a message contract with a class or build a class based on a message contract, schema providers give you the flexibility needed to get the job done.

Conclusion
By using schema providers along with the IXmlSerizable interface, you gain flexibility and control over the data contract and the serialization/deserialization process. Mapping non-public class members to a class's data contract is now possible and schema providers allow all the capabilities of XML Schemas to be used within the data contract of a class. Additionally, classes that are used as input parameters and return values of Web services can be equipped with schema providers in order to provide greater control over the Web service's message contract. Finally, the IXmlSerializable interface is now supported and provides access to all of an object's members. This interface can also be used to invoke custom business logic during the serialization/deserialization process.
Implementation is not as easy as with the .NET attribution model. The .NET attribution model is still supported in the .NET Framework 2.0, and you will need to choose the correct tool for each design scenario that requires interoperability.

Keith Pijanowski is a .NET Architect Evangelist for Microsoft Corporation specializing in the .NET Framework. Keith helps Microsoft customers apply new technologies to business problems. Keith is also a frequent speaker at Microsoft events and community events in the New Jersey and New York area.

Page view tracker