Kirk Evans Blog

.NET From a Markup Perspective

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.