Wednesday, May 5, 2010

Correctly Creating Classes Using xsd.exe

Not a lot of people are familiar with the xsd.exe that ships with Visual Studio. It allows you to create XML/classes from XML/classes. The following are the 4.0 xsd.exe capabilities:
Generates an XML schema from an XML-Data-Reduced schema file. XDR is an early XML-based schema format.
Generates an XML schema from an XML file.
XSD to DataSet
Generates common language runtime DataSet classes from an XSD schema file. The generated classes provide a rich object model for regular XML data.
XSD to Classes
Generates runtime classes from an XSD schema file. The generated classes can be used in conjunction with System.Xml.Serialization.XmlSerializer to read and write XML code that follows the schema.
Classes to XSD
Generates an XML schema from a type or types in a runtime assembly file. The generated schema defines the XML format used by System.Xml.Serialization.XmlSerializer.
It I’m a big fan of using xml schemas (XSDs) to generate classes that can be used to serialize and deserialize objects to and from XML (the XSD to Classes functionality listed above). If your schema changes, just rerunning xsd.exe for the correct schema updates the classes. No manually changes have to be made, including serialization code. It’s a beautiful thing. The problem has been, how do you set up your classes to automatically recompile with changes to the schema, and how do you deal with schemas that import other schemas?

XSDs Importing other XSDs

A common issue that developers of XSDs run into is violating the DRY principle repeatedly with XSD types. For example, let’s say you create a calendar meeting request service that has two XSDs, one for the request XML and one for the response XML. You’ve defined a xs:complexType “Meeting” that includes the date and location:
  <xs:complexType name="Meeting">
      <xs:element name="Location" type="xs:string"/>
      <xs:element name="Date" type="xs:date"/>
But you want to use it in both the request and the response XML. You could just copy and past it into both XSD files, and it will validate just fine, but if you use xsd.exe to generate your classes, it’s going to create two classes of type Meeting, which will cause a compiler error. You could have a separate namespace for each class, but then you’re definitely violating DRY. The answer is to place the Meeting type in a separate XSD and then reference it from both your request and your response XSD. This results in the XSDs below

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="Types"
  <xs:complexType name="Meeting">
      <xs:element name="Location" type="xs:string"/>
      <xs:element name="Date" type="xs:date"/>

<?xml version="1.0" encoding="utf-8" ?>
<xs:schema elementFormDefault="qualified" xmlns:xs="" xmlns:myTypes="">
  <xs:import namespace="" schemaLocation="Types.xsd" />
  <xs:element name="Request">
        <xs:element name="RqstMeeting" type="myTypes:Meeting"/>
        <xs:element name="RqstName" type="xs:string"/>

<?xml version="1.0" encoding="utf-8" ?>
<xs:schema elementFormDefault="qualified" xmlns:xs="" xmlns:myTypes="">
  <xs:import namespace="" schemaLocation="Types.xsd" />
  <xs:element name="Response">
        <xs:element name="Accepted" type="xs:boolean"/>
        <xs:element name="AlternateMeeting" type="myTypes:Meeting" minOccurs="0"/>

Now we’ve defined our Meeting type in one file, and reused it in both our Request.xsd and Response.xsd.

Getting xsd.exe To Import XSDs

Now that the type has been defined in another file, the xsd.exe will generate this error if you attempt to create the create the Request XML:
C:\Solution\Project>xsd.exe Request.xsd /c
Schema validation warning: Type '' is not declared.
Warning: Schema could not be validated. Class generation may fail or may produce incorrect results.
Error: Error generating classes for schema 'C:\Solution\Projects\Request'.

- The datatype '' is missing.
If you would like more help, please type "xsd /?".
This is due to the fact that the xsd.exe does not use the schemaLocation hint to find the imported schema. You’ve got to include it as a parameter. in your xsd.exe call:
C:\Solution\Project>xsd.exe Types.xsd Request.xsd /c
This will generate one file call Request.cs that has a Request class, and a Meeting class. Now we just need to create the Response class and we’re good to go. But wait… running “C:\Solution\Project>xsd.exe Types.xsd Response.xsd /c” will create a different file, Response.cs, that contains a Response class and a duplicate Meeting class. Now we’re stuck with another compiler error and no longer DRY.

Getting xsd.exe To Not Create Duplicate Classes

This is a simple fix, but it took me a long time to figure out. You have to use xsd.exe to compile all of your classes at once, so rather than running two separate commands, you just need to run one:
C:\Solution\Project>xsd.exe Types.xsd Request.xsd Response.xsd /c
Now you have one file, Response.xsd, with all three classes in it.

Getting Visual Studio 2010 To Auto Recompile XSD Generated Classes

Using the Project Build Events, you can set the project to always recompile the XSD classes each time you build the project. It is also helpful to rename the file so it isn’t always the name of the last XSD file passed to xsd.exe. Here are the Pre-build event command line values required to auto build the XSD classes and rename the file to XsdGeneratedClasses.cs:

EDIT suggested by Jamie instead of the Registry hack (it worked back in the day for me, don't know if it still does), he suggests using "$(TargetFrameworkSDKToolsDirectory)xsd.exe" to find the path of the xsd.exe. Thanks Jamie!

"$(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v7.0A\@InstallationFolder)\bin\NETFX 4.0 Tools\xsd.exe" "$(ProjectDir)Request.xsd" "$(ProjectDir)Response.xsd" "$(ProjectDir)Types.xsd" /c /o:"$(ProjectDir)"
move "$(ProjectDir)Types.cs" "$(ProjectDir)XsdGeneratedClasses.cs"

Now whenever the project get’s built, the XSD generated classes will always be rewritten by xsd.exe

Extending XSD Generated Classes

Don’t forget that classes created by xsd.exe are all partial classes. It’s a good idea to add default constructors and logic in a separate partial class in a different file. It’s especially helpful for initializing arrays since xsd.exe generated classes use arrays and not ArrayLists or Generic Lists. This allows you to add logic, that won’t be changed when the class is regenerated.

Serializing/Deserializing XSD Generated Classes

Now your code for Serializing and Deserializing your objects is as simple as this:
To Serialize:

XmlSerializer s = new XmlSerializer(typeof(Request));System.IO.TextWriter w = new System.IO.StreamWriter(@"C:\Request.xml");s.Serialize(w, new Request());w.Close();

To Deserialize:

XmlSerializer s = new XmlSerializer(typeof(Request));Request request;System.IO.TextReader r = new System.IO.StreamReader("request.xml");request = (Request)s.Deserialize(r);r.Close();


lx said...

Great post! thanks

Jamie said...

In the "Getting Visual Studio 2010 To Auto Recompile XSD Generated Classes", hard-coding the regpath to get xsd.exe's path is bad juju. Better to use $(TargetFrameworkSDKToolsDirectory).

Below is how I accomplish this in my csproj. I'll change open to '[' and close to ']' since this post insists on treating this as html. Also, you have to encode the quotes around the xsd.exe command, but again, they're just shown as quotes in this post.

[Target Name="GenerateSerializationClasses" Inputs="CompilerConfigSchema.xsd" Outputs="CompilerConfigSchema.cs"]
[Exec Command=""$(TargetFrameworkSDKToolsDirectory)xsd.exe" CompilerConfigSchema.xsd /c /n:GeographicBackgroundCompiler" /]
[Target Name="BeforeBuild" DependsOnTargets="GenerateSerializationClasses"]
[Target Name="CleanGeneratedFiles"]
[Delete Files="$(MSBuildProjectDirectory)\CompilerConfigSchema.cs" /]
[!-- Need to remove our generated file on clean. --]

Change CompilerConfigSchema to your xsd name.

Jamie said...

Oh, here's another thing that can hopefully save someone else from pulling their hair out.

You need to keep your generated files from being checked into your source control system. There is a file in your project directory with the extension .vspscc. Here is a sample that keeps a file called CodedTranslationList.cs from being checked in:

"FILE_VERSION" = "9237"
"EXCLUDED_FILE0" = "CodedTranslationList.cs"

Oh, and the empty quotes at the top of the contents are correct.