Using XML Schema Import and Export for DataContractSerializer
For many WCF developers, service metadata import and export is a magical thing. You start your service with a serviceMetadataBehavior, set httpGetEnabled to true or add an IMetadataExchange endpoint, and metadata gets automatically generated for you. Then you run svcutil or use Add Service Reference, and you get proxy code that just works without you having to delve into the details of how or why it even works to begin with. But it’s always worth going into details of how a framework does what it does. It allows you to customize WCF to suit your needs, and gives you a better understanding of your own software. I’ll attempt to demystify an important part of how metadata import and export works in today’s blog post.
SOA Metadata: WSDLs and XSDs
Metadata can be divided up into two different types:
1) The metadata for your service, describing everything about the service: its operations, messages, binding, etc…
2) The metadata that describes the data types you’ll be sending over the wire as XML schemas.
While it is possible to include XML schemas in a WSDL file through the <wsdl:types> section of a wsdl, WCF makes the distinction very clear by instead referencing separate schemas by using the <xs:import> tag. Using “svcutil /t:metadata” for example, you’ll see that you get exactly one wsdl file for a service, and multiple xsd files for each targetNamespace that needs to be defined.
I’ll be playing around with the second type of metadata in this post, and showing specifically how it can be used for DataContractSerializer.
Metadata Import
Let’s reuse the type I happened to use in my last post:
[DataContract]
public class Animal
{
[DataMember]
public int age = 4;
[DataMember]
public string name = "Rusty";
}
[DataContract]
public class Dog : Animal
{
[DataMember]
public DogBreed breed = DogBreed.LabradorRetriever;
}
public enum DogBreed {
GermanShepherd,
LabradorRetriever
}
Now, you have a DataContract that you’d like to see the XSDs for. How do you do that? It’s actually very simple:
XsdDataContractExporter exporter = new XsdDataContractExporter();
exporter.Export(typeof(Dog));
foreach (XmlSchema schema in exporter.Schemas.Schemas())
{
schema.Write(Console.Out);
Console.WriteLine("\n");
}
You should find three schemas in the output. Let’s go through them:
Schema 1:
<?xml version="1.0" encoding="IBM437"?>
<xs:schema xmlns:tns="https://schemas.microsoft.com/2003/10/Serialization/" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="https://schemas.microsoft.com/2003/10/Serialization/" xmlns:xs="https://www.w3.org/2001/XMLSchema">
<xs:element name="anyType" nillable="true" type="xs:anyType" />
<xs:element name="anyURI" nillable="true" type="xs:anyURI" />
<xs:element name="base64Binary" nillable="true" type="xs:base64Binary" />
<xs:element name="boolean" nillable="true" type="xs:boolean" />
<xs:element name="byte" nillable="true" type="xs:byte" />
<xs:element name="dateTime" nillable="true" type="xs:dateTime" />
<xs:element name="decimal" nillable="true" type="xs:decimal" />
<xs:element name="double" nillable="true" type="xs:double" />
<xs:element name="float" nillable="true" type="xs:float" />
<xs:element name="int" nillable="true" type="xs:int" />
<xs:element name="long" nillable="true" type="xs:long" />
<xs:element name="QName" nillable="true" type="xs:QName" />
<xs:element name="short" nillable="true" type="xs:short" />
<xs:element name="string" nillable="true" type="xs:string" />
<xs:element name="unsignedByte" nillable="true" type="xs:unsignedByte" />
<xs:element name="unsignedInt" nillable="true" type="xs:unsignedInt" />
<xs:element name="unsignedLong" nillable="true" type="xs:unsignedLong" />
<xs:element name="unsignedShort" nillable="true" type="xs:unsignedShort" />
<xs:element name="char" nillable="true" type="tns:char" />
<xs:simpleType name="char">
<xs:restriction base="xs:int" />
</xs:simpleType>
<xs:element name="duration" nillable="true" type="tns:duration" />
<xs:simpleType name="duration">
<xs:restriction base="xs:duration">
<xs:pattern value="\-?P(\d*D)?(T(\d*H)?(\d*M)?(\d*(\.\d*)?S)?)?" />
<xs:minInclusive value="-P10675199DT2H48M5.4775808S" />
<xs:maxInclusive value="P10675199DT2H48M5.4775807S" />
</xs:restriction>
</xs:simpleType>
<xs:element name="guid" nillable="true" type="tns:guid" />
<xs:simpleType name="guid">
<xs:restriction base="xs:string">
<xs:pattern value="[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}" />
</xs:restriction>
</xs:simpleType>
<xs:attribute name="FactoryType" type="xs:QName" />
<xs:attribute name="Id" type="xs:ID" />
<xs:attribute name="Ref" type="xs:IDREF" />
</xs:schema>
This is the WCF DataContractSerializer schema. It should be included in the metadata for any DataContract, or for any service that uses DataContratSerializer. As you can see, it maps various DataContractSerializer primitives into XML primitives, or into XML data types. The second schema is more interesting:
Schema 2:
<?xml version="1.0" encoding="IBM437"?>
<xs:schema xmlns:tns="https://schemas.datacontract.org/2004/07/" elementFormDefault="qualified" targetNamespace="https://schemas.datacontract.org/2004/07/" xmlns:xs="https://www.w3.org/2001/XMLSchema">
<xs:complexType name="Dog">
<xs:complexContent mixed="false">
<xs:extension base="tns:Animal">
<xs:sequence>
<xs:element minOccurs="0" name="breed" type="tns:DogBreed" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:element name="Dog" nillable="true" type="tns:Dog" />
<xs:complexType name="Animal">
<xs:sequence>
<xs:element minOccurs="0" name="age" type="xs:int" />
<xs:element minOccurs="0" name="name" nillable="true" type="xs:string" />
</xs:sequence>
</xs:complexType>
<xs:element name="Animal" nillable="true" type="tns:Animal" />
<xs:simpleType name="DogBreed">
<xs:restriction base="xs:string">
<xs:enumeration value="GermanShepherd" />
<xs:enumeration value="LabradorRetriever" />
</xs:restriction>
</xs:simpleType>
<xs:element name="DogBreed" nillable="true" type="tns:DogBreed" />
</xs:schema>
This is the schema for our types. I’ve highlighted some interesting parts. Notice how xs:extension is used to indicate that Dog inherits from Animal. And notice how the DogBreed enum is mapped into a simpleType restriction with xs:enumeration values.
And finally, we have one last small schema that defines the “schema” element.
Schema 3:
<?xml version="1.0" encoding="IBM437"?>
<xs:schema targetNamespace="https://www.w3.org/2001/XMLSchema" xmlns:xs="https://www.w3.org/2001/XMLSchema">
<xs:element name="schema">
<xs:complexType />
</xs:element>
</xs:schema>
Metadata Export
Now that your DataContract has been turned into an XML schema definition, you might be wondering “Well, how do I get my DataContract back?”. And the answer to that is pretty simple as well. You use XsdDataContractImporter to get a CodeCompileUnit out of the schemas. And then use your CodeProvider of choice (we’ll use the C# provider in this case) to get the code for your DataContract back from the XSD.
Add the following to your code:
XsdDataContractExporter exporter = new XsdDataContractExporter();
exporter.Export(typeof(Dog));
foreach (XmlSchema schema in exporter.Schemas.Schemas())
{
schema.Write(Console.Out);
Console.WriteLine("\n");
}
Console.WriteLine("-------------------------------------------");
XsdDataContractImporter importer = new XsdDataContractImporter();
importer.Import(exporter.Schemas);
CSharpCodeProvider provider = new CSharpCodeProvider();
provider.GenerateCodeFromCompileUnit(importer.CodeCompileUnit, Console.Out, null);
Now you get:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:2.0.50727.3074
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System.Runtime.Serialization;
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="Animal", Namespace="https://schemas.datacontract.org/2004/07/")]
[System.Runtime.Serialization.KnownTypeAttribute(typeof(Dog))]
public partial class Animal : object, System.Runtime.Serialization.IExtensibleDataObject {
private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
private int ageField;
private string nameField;
public System.Runtime.Serialization.ExtensionDataObject ExtensionData {
get {
return this.extensionDataField;
}
set {
this.extensionDataField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public int age {
get {
return this.ageField;
}
set {
this.ageField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public string name {
get {
return this.nameField;
}
set {
this.nameField = value;
}
}
}
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="Dog", Namespace="https://schemas.datacontract.org/2004/07/")]
public partial class Dog : Animal {
private DogBreed breedField;
[System.Runtime.Serialization.DataMemberAttribute()]
public DogBreed breed {
get {
return this.breedField;
}
set {
this.breedField = value;
}
}
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="DogBreed", Namespace="https://schemas.datacontract.org/2004/07/")]
public enum DogBreed : int {
[System.Runtime.Serialization.EnumMemberAttribute()]
GermanShepherd = 0,
[System.Runtime.Serialization.EnumMemberAttribute()]
LabradorRetriever = 1,
}
This generated DataContract is exactly equivalent to the DataContract you started off with! Dog inherits from Animal, and the same members are still there. All the fields have been wrapped with properties, but the property name is still the same as the original field name. Finally, you might also notice a member of type ExtensionDataObject on Animal. This is a member used for the deserialization of versioning information. If the serializer doesn’t recognize a member that’s being sent on the wire , it gets added to the ExtensionDataObject.
I hope I’ve helped to clarify what happens when you generate or consume metadata from a WCF service. It should be pretty obvious how WCF makes use of XsdDataContractExporter and XsdDataContractImporter after this.
Finally, if you'd like to learn how to import and export schemas for types you'll be serializing with XmlSerializer, please take a look at this post.
Comments
Anonymous
April 29, 2009
PingBack from http://microsoft-sharepoint.simplynetdev.com/using-xml-schema-import-and-export-for-datacontractserializer/Anonymous
May 12, 2009
Thank you for submitting this cool story - Trackback from DotNetShoutout