Ode to the SchemaImporterExtension Class

Back in May 2004, Daniel Cazzulino - XML wonk of the .NET blogosphere - published an article on MSDN entitled, "Code Generation in the .NET Framework Using XML Schema". In his article, he described how one could extend xsd.exe to support dynamic code generation when processing XML Schema. The purpose was to support the scenario where a developer could customize the code generated for schema type structures. In my opinion, this solution provided two benefits: (1) it provided control over the code generation process and, in turn, (2) resolved the "loose" type map inferred by the .NET Framework. Personally speaking, I can recall numerous occasions when the .NET Framework would infer type structure element as xsd:string, rather than its actual representation. System.Guid anyone? Not ideal, to say the least.

Fast forward to May 2005. In Visual Studio 2005, the name of the game is extensibility - this includes the tools that ship along side the SDK. In particular, the XML Schema Definition Tool (xsd.exe) has been updated to support schema importer extensions, a new feature of the .NET Framework. Let's take a closer look.

In the .NET Framework 2.0, a new namespace has been added; System.Xml.Serialization.Advanced. The purpose of this namespace is to contain types that support advanced XML serialization and schema generation scenarios. Currently, this namespace contains the following types: 

  • SchemaImporterExtension
  • SchemaImporterExtensionCollection 

The SchemaImporterExtension class allows developers to hook into the schema generation process of xsd.exe. By implementing a class that derives from this type, you can control the code generated for types found in schema.

Creating a schema importer extension is pretty easy. You simply derive from SchemaImporterExtension and override the appropriate methods. In most cases, you'll override the ImportSchemaType methods, which perform most of the heavy lifting. Here's an example:

// PersonSchemaImporterExtension.cs
 
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Xml.Serialization.Advanced;
 
namespace Bristowe.Examples.Xml.Serialization
{
 public sealed class PersonSchemaImporterExtension : SchemaImporterExtension
 {
  public override string ImportSchemaType(string name, string ns,
   XmlSchemaObject context, XmlSchemas schemas, XmlSchemaImporter importer,
   CodeCompileUnit compileUnit, CodeNamespace mainNamespace,
   CodeGenerationOptions options, CodeDomProvider codeProvider)
  {
   if (name.Equals("Person") && ns.Equals("http://bristowe.com/"))
   {
    CodeTypeDeclaration speaker = new CodeTypeDeclaration("Person");
    mainNamespace.Types.Add(speaker);
 
    CodeMemberField firstNameField = new CodeMemberField(
     new CodeTypeReference(typeof(string)), "_firstName");
    speaker.Members.Add(firstNameField);
 
    CodeMemberProperty firstNameProperty = new CodeMemberProperty();
    firstNameProperty.Attributes = MemberAttributes.Public | MemberAttributes.Final;
    firstNameProperty.GetStatements.Add(
     new CodeMethodReturnStatement(
      new CodeFieldReferenceExpression(
       new CodeThisReferenceExpression(), "_firstName")));
    firstNameProperty.Name = "FirstName";
    firstNameProperty.SetStatements.Add(
     new CodeAssignStatement(
      new CodeFieldReferenceExpression(
       new CodeThisReferenceExpression(), "_firstName"),
       new CodePropertySetValueReferenceExpression()));
    firstNameProperty.Type = new CodeTypeReference(typeof(string));
    speaker.Members.Add(firstNameProperty);
 
    CodeMemberField surnameField = new CodeMemberField(
     new CodeTypeReference(typeof(string)), "_surname");
    speaker.Members.Add(surnameField);
 
    CodeMemberProperty surnameProperty = new CodeMemberProperty();
    surnameProperty.Attributes = MemberAttributes.Public | MemberAttributes.Final;
    surnameProperty.GetStatements.Add(
     new CodeMethodReturnStatement(
      new CodeFieldReferenceExpression(
       new CodeThisReferenceExpression(), "_surname")));
    surnameProperty.Name = "Surname";
    surnameProperty.SetStatements.Add(new CodeAssignStatement(
     new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_surname"),
     new CodePropertySetValueReferenceExpression()));
    surnameProperty.Type = new CodeTypeReference(typeof(string));
    speaker.Members.Add(surnameProperty);
 
    return "Person";
   }
 
   return null;
  }
 }
}

Here's how it works: Schema importer extensions form a pipeline of processing units. If/When an extension finds a type it is responsible for generating custom code, it does so via the Code DOM provided by the .NET Framework. In the code example above, you can see that the only type this extension can process is the element, Person that resides in the namespace, "http://bristowe.com/".

Here's how the process looks from a 50,000' view:

When the XML Schema Definition Tool (xsd.exe) is used to import a XML Schema, it uses the System.Xml.Serialization.XmlSchemaImporter class internally to process any/all schema elements found in the XML Schema document (foo.xsd). When created, the XmlSchemaImporter class loads any schema importer extensions defined in the underlying configuration. In this instance, leveraging machine.config is the default usage pattern. Here's how a schema importer extension is registered in machine.config:

<configuration>
 ...
 <system.xml.serialization>
  <schemaImporterExtensions>
   <add
    name="Bristowe.Examples.Xml.Serialization.PersonSchemaImporterExtension"
    type="Bristowe.Examples.Xml.Serialization.PersonSchemaImporterExtension,
     Bristowe.Examples.Xml,
     Version=1.0.0.0"
   />
  </schemaImporterExtensions>
 </system.xml.serialization>
</configuration>

With these extensions loaded, xsd.exe is ready to process the XML Schema. Now when the tool encounters your type (i.e. bristowe:Person), your custom code will be added to the generated class file. Neat, eh?