Condividi tramite


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:

?

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 // From https://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));         }     } } #>

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: https://phazed.com/]