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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
<#@ 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
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));
        }
    }
}
#>

Initial projectsT4 template added with output filesFor 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!

 

 

 

 

[Originally posted by Rupert Benbrook on 28th August 2010 here: http://phazed.com/]