Auto-generating XML serialization classes from BizTalk schemas
When building BizTalk applications you often need to generate the XML serialization classes for your schemas. Sometimes this is a more efficient and simple way gain access to data and manipulate or construct complex messages than maps, distinguished fields, or XPath expressions. The normal way to achieve this is to run the XML Schema Definition tool (XSD.exe) on your schemas to generate the classes that represent them. However, this means leaving the comfort of the Visual Studio environment for the Command Prompt, and during the early phases of projects, when schemas are often in a state of flux, this becomes a right hassle. Believe me, the last thing you want to happen in a team of BizTalk developers is for the schemas and their serialization classes to get out of step!
I’ve been looking at different ways to solve this problem, and at first I started with MSBuild calling out to XSD.exe. This approach wasn’t very satisfactory as it was a bit clunky and required editing project files and the like. So I took a different tack and decided to use a T4 template.
If you haven’t come across T4 templates in Visual Studio then they’re probably it’s best kept secret. T4 actually stands for Text Template Transformation Toolkit, and it’s a very powerful code generation tool. I won’t spend time describing it in detail in this post as Scott Hanselman has a good introduction on his blog, and Oleg Synch has a great set of tutorial articles on it. Put simply, T4 lets me write some code that will generate some code for me, in this case the XML serialization classes for some schemas.
The first step was to work out how to do the same job as XSD.exe, but from code. Fortunately Mike Hadlow had been there before me. Next, I needed to put all this in a template, but I wanted to go a step further than just creating the XML serialization classes; I also wanted to generate serialize and deserialize methods for each of the root elements in each of the schemas. To do this I’d need to create multiple output files from T4, something it doesn’t do normally. Again, someone has been here before, this time Damien Guard with his excellent T4 Manager class. Now, armed with all this I could put the template together, and here it is:
<#@ template language="C#v3.5" hostSpecific="true" debug="false" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data.Linq" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ import namespace="System" #>
<#@ import namespace="System.CodeDom" #>
<#@ import namespace="System.CodeDom.Compiler" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Xml.Serialization" #>
<#@ import namespace="System.Xml.Schema" #>
<#@ import namespace="Microsoft.CSharp" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<# var manager = Manager.Create(Host, GenerationEnvironment); #>
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:<#=Environment.Version.ToString()#>
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
//
// This source code was auto-generated by XmlSerializer.tt.
//
<#
IServiceProvider hostServiceProvider = (IServiceProvider)Host;
EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
EnvDTE.ProjectItem templateProjectItem = dte.Solution.FindProjectItem(Host.TemplateFile);
EnvDTE.Project project = templateProjectItem.ContainingProject;
XmlSchemas xsds = new XmlSchemas();
foreach (EnvDTE.ProjectItem projectItem in GetAllItems(project.ProjectItems.Cast<EnvDTE.ProjectItem>()))
{
string path = projectItem.get_FileNames(0);
string directory = Path.GetDirectoryName(path);
if (path.EndsWith(".xsd"))
{
using (FileStream stream = File.OpenRead(path))
{
XmlSchema xsd = XmlSchema.Read(stream, null);
xsds.Add(xsd);
foreach(XmlSchemaElement schemaElement in xsd.Elements.Values)
{
manager.StartNewFile(schemaElement.Name + ".Serialization.cs");
#>
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace <#= project.Properties.Item("DefaultNamespace").Value.ToString() #>
{
public partial class <#= schemaElement.Name #>
{
public static <#= schemaElement.Name #> Deserialize(Stream stream)
{
var serializer = new XmlSerializer(typeof(<#= schemaElement.Name #>));
return (<#= schemaElement.Name #>)serializer.Deserialize(stream);
}
public static <#= schemaElement.Name #> Deserialize(TextReader reader)
{
var serializer = new XmlSerializer(typeof(<#= schemaElement.Name #>));
return (<#= schemaElement.Name #>)serializer.Deserialize(reader);
}
public static <#= schemaElement.Name #> Deserialize(XmlReader reader)
{
var serializer = new XmlSerializer(typeof(<#= schemaElement.Name #>));
return (<#= schemaElement.Name #>)serializer.Deserialize(reader);
}
public void Serialize(Stream stream)
{
var serializer = new XmlSerializer(typeof(<#= schemaElement.Name #>));
serializer.Serialize(stream, this);
}
public void Serialize(TextWriter writer)
{
var serializer = new XmlSerializer(typeof(<#= schemaElement.Name #>));
serializer.Serialize(writer, this);
}
public void Serialize(XmlWriter writer)
{
var serializer = new XmlSerializer(typeof(<#= schemaElement.Name #>));
serializer.Serialize(writer, this);
}
}
}
<#
manager.EndBlock();
}
}
}
}
xsds.Compile(null, true);
XmlSchemaImporter schemaImporter = new XmlSchemaImporter(xsds);
CodeNamespace codeNamespace = new CodeNamespace(project.Properties.Item("DefaultNamespace").Value.ToString());
XmlCodeExporter codeExporter = new XmlCodeExporter(codeNamespace);
List<XmlTypeMapping> maps = new List<XmlTypeMapping>();
foreach (XmlSchema xsd in xsds)
{
foreach(XmlSchemaType schemaType in xsd.SchemaTypes.Values)
{
maps.Add(schemaImporter.ImportSchemaType(schemaType.QualifiedName));
}
foreach(XmlSchemaElement schemaElement in xsd.Elements.Values)
{
maps.Add(schemaImporter.ImportTypeMapping(schemaElement.QualifiedName));
}
}
foreach(XmlTypeMapping map in maps)
{
codeExporter.ExportTypeMapping(map);
}
CodeGenerator.ValidateIdentifiers(codeNamespace);
CSharpCodeProvider codeProvider = new CSharpCodeProvider();
using(StringWriter writer = new StringWriter(GenerationEnvironment))
{
codeProvider.GenerateCodeFromNamespace(codeNamespace, writer, new CodeGeneratorOptions());
}
manager.Process(true);
#>
<#+
private IEnumerable<EnvDTE.ProjectItem> GetAllItems(IEnumerable<EnvDTE.ProjectItem> projectItems)
{
return projectItems.Concat(projectItems.SelectMany(i => GetAllItems(i.ProjectItems.Cast<EnvDTE.ProjectItem>())));
}
// Manager class records the various blocks so it can split them up
// From http://damieng.com/blog/2009/11/06/multiple-outputs-from-t4-made-easy-revisited
class Manager {
private class Block {
public String Name;
public int Start, Length;
}
private Block currentBlock;
private List<Block> files = new List<Block>();
private Block footer = new Block();
private Block header = new Block();
private ITextTemplatingEngineHost host;
private StringBuilder template;
protected List<String> generatedFileNames = new List<String>();
public static Manager Create(ITextTemplatingEngineHost host, StringBuilder template) {
return (host is IServiceProvider) ? new VSManager(host, template) : new Manager(host, template);
}
public void StartNewFile(String name) {
if (name == null)
throw new ArgumentNullException("name");
CurrentBlock = new Block { Name = name };
}
public void StartFooter() {
CurrentBlock = footer;
}
public void StartHeader() {
CurrentBlock = header;
}
public void EndBlock() {
if (CurrentBlock == null)
return;
CurrentBlock.Length = template.Length - CurrentBlock.Start;
if (CurrentBlock != header && CurrentBlock != footer)
files.Add(CurrentBlock);
currentBlock = null;
}
public virtual void Process(bool split) {
if (split) {
EndBlock();
String headerText = template.ToString(header.Start, header.Length);
String footerText = template.ToString(footer.Start, footer.Length);
String outputPath = Path.GetDirectoryName(host.TemplateFile);
files.Reverse();
foreach(Block block in files) {
String fileName = Path.Combine(outputPath, block.Name);
String content = headerText + template.ToString(block.Start, block.Length) + footerText;
generatedFileNames.Add(fileName);
CreateFile(fileName, content);
template.Remove(block.Start, block.Length);
}
}
}
protected virtual void CreateFile(String fileName, String content) {
if (IsFileContentDifferent(fileName, content))
File.WriteAllText(fileName, content);
}
public virtual String GetCustomToolNamespace(String fileName) {
return null;
}
public virtual String DefaultProjectNamespace {
get { return null; }
}
protected bool IsFileContentDifferent(String fileName, String newContent) {
return !(File.Exists(fileName) && File.ReadAllText(fileName) == newContent);
}
private Manager(ITextTemplatingEngineHost host, StringBuilder template) {
this.host = host;
this.template = template;
}
private Block CurrentBlock {
get { return currentBlock; }
set {
if (CurrentBlock != null)
EndBlock();
if (value != null)
value.Start = template.Length;
currentBlock = value;
}
}
private class VSManager: Manager {
private EnvDTE.ProjectItem templateProjectItem;
private EnvDTE.DTE dte;
private Action<String> checkOutAction;
private Action<IEnumerable<String>> projectSyncAction;
public override String DefaultProjectNamespace {
get {
return templateProjectItem.ContainingProject.Properties.Item("DefaultNamespace").Value.ToString();
}
}
public override String GetCustomToolNamespace(string fileName) {
return dte.Solution.FindProjectItem(fileName).Properties.Item("CustomToolNamespace").Value.ToString();
}
public override void Process(bool split) {
if (templateProjectItem.ProjectItems == null)
return;
base.Process(split);
projectSyncAction.EndInvoke(projectSyncAction.BeginInvoke(generatedFileNames, null, null));
}
protected override void CreateFile(String fileName, String content) {
if (IsFileContentDifferent(fileName, content)) {
CheckoutFileIfRequired(fileName);
File.WriteAllText(fileName, content);
}
}
internal VSManager(ITextTemplatingEngineHost host, StringBuilder template)
: base(host, template) {
var hostServiceProvider = (IServiceProvider) host;
if (hostServiceProvider == null)
throw new ArgumentNullException("Could not obtain IServiceProvider");
dte = (EnvDTE.DTE) hostServiceProvider.GetService(typeof(EnvDTE.DTE));
if (dte == null)
throw new ArgumentNullException("Could not obtain DTE from host");
templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile);
checkOutAction = (String fileName) => dte.SourceControl.CheckOutItem(fileName);
projectSyncAction = (IEnumerable<String> keepFileNames) => ProjectSync(templateProjectItem, keepFileNames);
}
private static void ProjectSync(EnvDTE.ProjectItem templateProjectItem, IEnumerable<String> keepFileNames) {
var keepFileNameSet = new HashSet<String>(keepFileNames);
var projectFiles = new Dictionary<String, EnvDTE.ProjectItem>();
var originalFilePrefix = Path.GetFileNameWithoutExtension(templateProjectItem.get_FileNames(0)) + ".";
foreach(EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems)
projectFiles.Add(projectItem.get_FileNames(0), projectItem);
// Remove unused items from the project
foreach(var pair in projectFiles)
if (!keepFileNames.Contains(pair.Key) && !(Path.GetFileNameWithoutExtension(pair.Key) + ".").StartsWith(originalFilePrefix))
pair.Value.Delete();
// Add missing files to the project
foreach(String fileName in keepFileNameSet)
if (!projectFiles.ContainsKey(fileName))
templateProjectItem.ProjectItems.AddFromFile(fileName);
}
private void CheckoutFileIfRequired(String fileName) {
var sc = dte.SourceControl;
if (sc != null && sc.IsItemUnderSCC(fileName) && !sc.IsItemCheckedOut(fileName))
checkOutAction.EndInvoke(checkOutAction.BeginInvoke(fileName, null, null));
}
}
}
#>

For simplicity’s sake I’ve merged Damien’s T4 Manager class into this template, just so there’s a single file to drop into the project. So, how do you use this template? Well first create a BizTalk project with some schemas, and then create a C# class library project alongside it; this project is going to contain the XML serialization classes. Next, add the schemas from the BizTalk project into the C# class library project, but as a link so they don’t get copied into the project, just referred to. Now just add the template, which I’ve named XmlSerializer.tt, to the project. This will automatically generate XmlSerializer.cs, which contains the XML serialization classes, and also *.Serialization.cs for each of the root elements in each of the schema files. It couldn’t be easier! There’s only one thing to watch out for; the code generation only occurs when you save the template file or select Run Custom Tool from its context menu in Solution Explorer. So if you change your schemas you’ll need to remember to resave the template to regenerate the serialization code. This is just how T4 works out of the box, but it’s an awful lot easier than getting out a Command Prompt and remembering the syntax for XSD.exe!

