Web Services Contract First: Drop Schema in app_code with a Custom Build Provider

I now have complete respect for Fritz' reaction to custom build providers.

Web applications in Visual Studio 2005 allow you to drop items in the app_code directory in your project and classes are compiled in the background for you.  For instance, drop a WSDL file in there and you get a proxy for the web service, drop a schema and you get a DataSet.  I wanted to drop a schema in app_code and get an XML serializable class.

I cheated. 

I posted to an internal distribution list asking if anyone knew how to do this.  Rick Lievano pointed me to Fritz' article.  I then pinged Elena Kharitidi, and she referenced creating a custom build provider as well.  Hmm... now, how do I get that done?  Then I remembered Daniel Cazzulino's excellent article on XsdCodeGen.  A couple modifications between Fritz' blog posting and Daniel's article, and I had a jaw-dropping experience as well.

 using System;
using System.Text;
using System.Web.Compilation;
using System.Web;
using System.Web.UI;
using System.CodeDom;
using System.Web.Hosting;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace Msdn.Samples.Compilation
{    
    public class XsdClassBuildProvider : System.Web.Compilation.BuildProvider
    {
        public override void GenerateCode(AssemblyBuilder assemblyBuilder)
        {            
            // HACK: Need to update this to get namespace from schema somehow.  Good for blog code.
            string ns = "Evans.Test";            
            XmlSchema xsd = null;

            using (Stream schemaStream = base.OpenStream(base.VirtualPath))
            {                                
                xsd = XmlSchema.Read(schemaStream, null);
                XmlSchemaSet set = new XmlSchemaSet();
                set.Add(xsd);
                set.Compile();
            }

            XmlSchemas schemas = new XmlSchemas();
            schemas.Add(xsd);
            XmlSchemaImporter importer = new XmlSchemaImporter(schemas);

            CodeNamespace codeNamespace = new CodeNamespace(ns);

            // Generate a CodeCompileUnit from the dataset
            CodeCompileUnit codeCompileUnit = new CodeCompileUnit();

            codeCompileUnit.Namespaces.Add(codeNamespace);
            XmlCodeExporter exporter = new XmlCodeExporter(codeNamespace);

            foreach (XmlSchemaElement element in xsd.Elements.Values)
            {
                // Import the mapping first.
                XmlTypeMapping mapping = importer.ImportTypeMapping(
                  element.QualifiedName);
                // Export the code finally.
                exporter.ExportTypeMapping(mapping);
            }

            // Add the CodeCompileUnit to the compilation
            assemblyBuilder.AddCodeCompileUnit(this, codeCompileUnit);
        }
    }
}

I built the class and added a reference from a test web service project.  Since there is already a BuildProvider registered for files with .XSD by default, you need to remove the existing mapping then add in your custom provider.  In the web service project, I added the following to configuration/system.web/compilation:

<compilation debug="false">
 <buildProviders>
  <remove extension=".xsd"/>
  <add appliesTo="Code" extension=".xsd" type="Msdn.Samples.Compilation.XsdClassBuildProvider"/>
 </buildProviders>
</compilation>

That was too easy.  I created a very simple schema and added it to the app_code directory in a web service:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="test" targetNamespace="http://tempuri.org/test.xsd" elementFormDefault="qualified" xmlns="http://tempuri.org/test.xsd" xmlns:mstns="http://tempuri.org/test.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:complexType name="customerType">
        <xs:sequence>
            <xs:element name="customerID" type="xs:string" />
            <xs:element name="customerName" type="xs:string" />
        </xs:sequence>
    </xs:complexType>
    <xs:element name="customer" type="customerType">
    </xs:element>
</xs:schema>

Here is what the screen looked like when my jaw hit the floor as well.

 

Custom build provider in ASP.NET that provides strongly-typed access to XML serializable schemas

The reason this is so cool is because it removes the extra step of dropping back to command-line with XSD.EXE to use the /classes switch.  Now, you just drop the schema in there and you are good to go.