Building a Better WSDL.EXE

Introduction

The other day, I blogged about how to generate WSDL directly from an assembly, without having to first set up an ASP.NET website so you can surf to the page with the "?wsdl" at the end. Well, as it turns out, it's pretty easy to take things a step further, and generate proxies directly from the assembly, without ever actually saving the WSDL to disk. I wrote that code for my client and they graciously allowed me to post it here for your benefit.

Explanation

The code itself is fairly simple, and appears below. At the heart of it are just a few short lines lines. Namely, these:

ServiceDescriptionReflector reflector = new ServiceDescriptionReflector(); reflector.Reflect(type, uri);

which generate the WSDL given a type that has one or more WebMethods on it, and these:

ServiceDescriptionImporter importer = new ServiceDescriptionImporter(); importer.AddServiceDescription(reflector.ServiceDescriptions[0], null, null); CodeNamespace codeNamespace = new CodeNamespace("Your.Proxy.Namespace.Here"); CodeCompileUnit codeCompileUnit = new CodeCompileUnit(); codeCompileUnit.Namespaces.Add(codeNamespace); ServiceDescriptionImportWarnings warnings = importer.Import(codeNamespace, codeCompileUnit);

Notice that the ServiceDescription generated by the ServiceDescriptionReflector feeds right into the ServiceDescriptionImporter, which is nice.

The rest of the code I have below does a bunch of other stuff:

    • Reflects over the input assembly to figure out which types in it have WebMethods.

    • Strips out all of the generated types that aren't the proxy itself. This is nice because we have several types that are shared across web services, so we build them into another assembly that we share in our client code. Not having them get generated by the proxy means we don't have all sorts of stupid name collisions.

    • Adds some using statements to the generated code, since it now relies on types defined elsewhere for parameters and return types.

    • Writes the files to disk.

I'm the first to admit this code isn't perfect. Partly that's because I'm new to using the CodeDOM APIs, but partly it's because I wrote it on my client's dime, so I went ahead and hardcoded it for our needs. For example, you probably want to use a different namespace for your generated proxies than we did. Still, I hope the code might be slightly useful for someone, so I'm posting it here. If you have any problems with it, contact me.aspx.

The Code

using System; using System.CodeDom; using System.CodeDom.Compiler; using System.Collections; using System.IO; using System.Reflection; using System.Web; using System.Web.Services; using System.Web.Services.Description; using System.Web.Services.Discovery; using System.Xml; using Microsoft.CSharp; namespace Integic.ePower.Psd.Common.DevTools.ProxyGenerator { public class App { static int Main(string[] args) { Console.WriteLine("Offline WSDL Generator. Copyright (c) 2004 Integic Corporation."); if (args.Length < 2) { Console.WriteLine(@"Usage: proxygen assembly URL"); return -1; } try { Assembly assembly = Assembly.LoadFrom(args[0]); // Find every type in the assembly that has a [WebMethod]. Generate proxies // for those types. foreach (Module module in assembly.GetModules()) { foreach (Type type in module.GetTypes()) { bool hasWebMethod = false; foreach (MethodInfo method in type.GetMethods()) { if (method.GetCustomAttributes(typeof(WebMethodAttribute), false).Length > 0) { hasWebMethod = true; break; } } if (hasWebMethod) { GenerateProxy(type, args[1], type.Name + ".cs"); } } } } catch (Exception e) { Console.WriteLine(e); return -1; } return 0; } /// <summary> /// Loop over all members that don't have a triple-slash comment and /// add one. We do this primarily so that the generated code won't barf /// with a "missing XML comment" when we turn the warning level up to 4 /// and set "warn as error" to true. /// </summary> /// <param name="codeCompileUnit">The CodeDOM object that represents the /// generated proxy code</param> private static void AddDocComments(CodeCompileUnit codeCompileUnit) { foreach (CodeNamespace codeNamespace in codeCompileUnit.Namespaces) { foreach (CodeTypeDeclaration type in codeNamespace.Types) { foreach (CodeTypeMember member in type.Members) { if (member.Comments.Count == 0) { member.Comments.Add(new CodeCommentStatement("<summary />", true)); } } } } } /// <summary> /// This is the method that actually spits the generated code out as a /// .cs file. /// </summary> /// <param name="codeCompileUnit">The CodeDOM object that represents the /// generated proxy code</param> /// <param name="path">Path to the file to generate</param> private static void EmitFile(CodeCompileUnit codeCompileUnit, string path) { CSharpCodeProvider provider = new CSharpCodeProvider(); ICodeGenerator generator = provider.CreateGenerator(); // Just chooses some formatting options, like four space indenting CodeGeneratorOptions options = new CodeGeneratorOptions(); options.BlankLinesBetweenMembers = true; options.BracingStyle = "C"; options.ElseOnClosing = false; options.IndentString = " "; StreamWriter writer = new StreamWriter(path); generator.GenerateCodeFromCompileUnit(codeCompileUnit, writer, options); writer.Close(); } /// <summary> /// This is the workhorse method of the program. Given a web service type, /// it generates a proxy class for it, strips out any excess types, and then /// adds a few using statments to it. /// </summary> /// <param name="type">The web service type</param> /// <param name="uri">The URL for the service that will be set in the constructor</param> /// <param name="path">The path to the .cs file that will be generated</param> private static void GenerateProxy(Type type, string uri, string path) { // These next two lines do the generate the WSDL based on the web service class ServiceDescriptionReflector reflector = new ServiceDescriptionReflector(); reflector.Reflect(type, uri); if (reflector.ServiceDescriptions.Count > 1) { throw new Exception(string.Format("Don't know how to deal with multiple service descriptions in {0}", type)); } // Now we take the WSDL service description and turn it into a proxy in CodeDOM form ServiceDescriptionImporter importer = new ServiceDescriptionImporter(); importer.AddServiceDescription(reflector.ServiceDescriptions[0], null, null); importer.Style = ServiceDescriptionImportStyle.Client; // Probably a good idea to make the namespace a command-line parameter, but hardcode it for now CodeNamespace codeNamespace = new CodeNamespace("Integic.ePower.Psd.WebServices.Common.Proxies"); CodeCompileUnit codeCompileUnit = new CodeCompileUnit(); codeCompileUnit.Namespaces.Add(codeNamespace); ServiceDescriptionImportWarnings warnings = importer.Import(codeNamespace, codeCompileUnit); Console.WriteLine("Generated proxy class for {0}. Warnings: {1}", type, warnings); // Pull out anything that isn't the proxy class itself StripTypes(codeCompileUnit); // Add any missing XML documentation comments AddDocComments(codeCompileUnit); // Serialize the generated code to disk EmitFile(codeCompileUnit, path); Console.WriteLine("Wrote proxy for {0} to {1}", type.FullName, path); } /// <summary> /// This method walks over the generated code, removing any code that isn't the proxy /// class itself. /// </summary> /// <param name="codeCompileUnit">The generated proxy code, in CodeDOM form. This /// object will be modified by this method.</param> private static void StripTypes(CodeCompileUnit codeCompileUnit) { foreach (CodeNamespace codeNamespace in codeCompileUnit.Namespaces) { // Remove anything that isn't the proxy itself ArrayList typesToRemove = new ArrayList(); foreach (CodeTypeDeclaration codeType in codeNamespace.Types) { bool webDerived = false; foreach (CodeTypeReference baseType in codeType.BaseTypes) { if (baseType.BaseType == "System.Web.Services.Protocols.SoapHttpClientProtocol") { webDerived = true; break; } } // We can't remove elements from a collection while we're iterating over it... if (!webDerived) { typesToRemove.Add(codeType); } } // ...so we remove them later foreach (CodeTypeDeclaration codeType in typesToRemove) { codeNamespace.Types.Remove(codeType); } // Add the missing using statements. Should probably allow these to be specified // on the command line, but hardcode them for now codeNamespace.Imports.Add(new CodeNamespaceImport("Integic.ePower.Psd.Common.Framework")); codeNamespace.Imports.Add(new CodeNamespaceImport("Integic.ePower.Psd.Common.Framework.Audit")); codeNamespace.Imports.Add(new CodeNamespaceImport("Integic.ePower.Psd.Common.Utilities.Ticketing")); } } } }

More Code

This version of the code was sent to me by Yuriy S. Musatenko (http://tetis.uazone.net/~yura). It sports a few usability improvements. He gave me the go-ahead to post it here. Enjoy!

using System; using System.CodeDom; using System.CodeDom.Compiler; using System.Collections; using System.IO; using System.Reflection; using System.Web; using System.Web.Services; using System.Web.Services.Description; using System.Web.Services.Discovery; using System.Xml; using Microsoft.CSharp; // Original code by CraigAndera's wiki at // http://www.pluralsite.com/wiki/default.aspx/Craig/RebuildingWsdlExe.html // Tool usability improvement by Yuriy S. Musatenko (http://tetis.uazone.net/~yura) namespace SmartWSDL.ProxyGenerator { public class CmdLineParams { public string AssemblyName; public string Uri = "http://tempuri.org/"; public string Namespace = null; public ArrayList UsingStatements = new ArrayList(); } public class App { static CmdLineParams par = new CmdLineParams(); static CmdLineParams ParseCommandLine(string[] args) { par.AssemblyName = args[0]; par.Uri = args[1]; int i; for(i = 1; i < args.Length; i++) { string arg = args[i]; if(arg[0] != '/') { throw new Exception("Incorrect command line parameter " + (i+1) + " :"+ arg); } switch(arg[1]) { case 'n': case 'N': par.Namespace = arg.Split(':')[1]; break; case 'u': case 'U': par.UsingStatements.Add(arg.Split(':')[1]); break; case 'r': case 'R': par.Uri = arg.Substring(3); break; default: throw new Exception("Incorrect command line parameter " + (i+1) + " :"+ arg); } } if(par.Namespace == null) { string[] split = par.AssemblyName.Split( new char[] {'/', '\\'}); string assemblyName = split[split.Length-1]; par.Namespace = assemblyName; if(assemblyName.ToUpper().LastIndexOf(".DLL") != -1) { par.Namespace = assemblyName.Substring(0, assemblyName.Length-4); } } return par; } static int Main(string[] args) { Console.WriteLine("Offline WSDL Generator. Copyright (c) 2004 Integic Corporation."); if (args.Length < 1) { Console.WriteLine(@"Usage: SmartWSDL assembly [/r:URI] [/n:Namespace] [/u:UsingNamespace] [/out:sourcecode]"); Console.WriteLine(@""); Console.WriteLine(@"/n:Namespace - Generaty proxy in namespace Namespace"); Console.WriteLine(@"/r:URI - Set web service XML namespace Uri"); Console.WriteLine("/u:UsingNamespace - Insert \"using UsingNamespace;\" statement into generated code"); Console.WriteLine(" may occur multiple times"); return -1; } par = ParseCommandLine(args); try { Assembly assembly = Assembly.LoadFrom(par.AssemblyName); // Find every type in the assembly that has a [WebMethod]. Generate proxies // for those types. foreach (Module module in assembly.GetModules()) { foreach (Type type in module.GetTypes()) { bool hasWebMethod = false; foreach (MethodInfo method in type.GetMethods()) { if (method.GetCustomAttributes(typeof(WebMethodAttribute), false).Length > 0) { hasWebMethod = true; break; } } if (hasWebMethod) { GenerateProxy(type, par.Uri, type.Name + ".cs"); } } } } catch (Exception e) { Console.WriteLine(e); return -1; } return 0; } /// <summary> /// Loop over all members that don't have a triple-slash comment and /// add one. We do this primarily so that the generated code won't barf /// with a "missing XML comment" when we turn the warning level up to 4 /// and set "warn as error" to true. /// </summary> /// <param name="codeCompileUnit">The CodeDOM object that represents the /// generated proxy code</param> private static void AddDocComments(CodeCompileUnit codeCompileUnit) { foreach (CodeNamespace codeNamespace in codeCompileUnit.Namespaces) { foreach (CodeTypeDeclaration type in codeNamespace.Types) { foreach (CodeTypeMember member in type.Members) { if (member.Comments.Count == 0) { member.Comments.Add(new CodeCommentStatement("<summary />", true)); } } } } } /// <summary> /// This is the method that actually spits the generated code out as a /// .cs file. /// </summary> /// <param name="codeCompileUnit">The CodeDOM object that represents the /// generated proxy code</param> /// <param name="path">Path to the file to generate</param> private static void EmitFile(CodeCompileUnit codeCompileUnit, string path) { CSharpCodeProvider provider = new CSharpCodeProvider(); ICodeGenerator generator = provider.CreateGenerator(); // Just chooses some formatting options, like four space indenting CodeGeneratorOptions options = new CodeGeneratorOptions(); options.BlankLinesBetweenMembers = true; options.BracingStyle = "C"; options.ElseOnClosing = false; options.IndentString = " "; StreamWriter writer = new StreamWriter(path); generator.GenerateCodeFromCompileUnit(codeCompileUnit, writer, options); writer.Close(); } /// <summary> /// This is the workhorse method of the program. Given a web service type, /// it generates a proxy class for it, strips out any excess types, and then /// adds a few using statments to it. /// </summary> /// <param name="type">The web service type</param> /// <param name="uri">The URL for the service that will be set in the constructor</param> /// <param name="path">The path to the .cs file that will be generated</param> private static void GenerateProxy(Type type, string uri, string path) { // These next two lines do the generate the WSDL based on the web service class ServiceDescriptionReflector reflector = new ServiceDescriptionReflector(); reflector.Reflect(type, uri); if (reflector.ServiceDescriptions.Count > 1) { throw new Exception(string.Format("Don't know how to deal with multiple service descriptions in {0}", type)); } // Now we take the WSDL service description and turn it into a proxy in CodeDOM form ServiceDescriptionImporter importer = new ServiceDescriptionImporter(); importer.AddServiceDescription(reflector.ServiceDescriptions[0], null, null); importer.Style = ServiceDescriptionImportStyle.Client; // Probably a good idea to make the namespace a command-line parameter, but hardcode it for now CodeNamespace codeNamespace = new CodeNamespace(par.Namespace); CodeCompileUnit codeCompileUnit = new CodeCompileUnit(); codeCompileUnit.Namespaces.Add(codeNamespace); ServiceDescriptionImportWarnings warnings = importer.Import(codeNamespace, codeCompileUnit); Console.WriteLine("Generated proxy class for {0}. Warnings: {1}", type, warnings); // Pull out anything that isn't the proxy class itself StripTypes(codeCompileUnit); // Add any missing XML documentation comments AddDocComments(codeCompileUnit); // Serialize the generated code to disk EmitFile(codeCompileUnit, path); Console.WriteLine("Wrote proxy for {0} to {1}", type.FullName, path); } /// <summary> /// This method walks over the generated code, removing any code that isn't the proxy /// class itself. /// </summary> /// <param name="codeCompileUnit">The generated proxy code, in CodeDOM form. This /// object will be modified by this method.</param> private static void StripTypes(CodeCompileUnit codeCompileUnit) { foreach (CodeNamespace codeNamespace in codeCompileUnit.Namespaces) { // Remove anything that isn't the proxy itself ArrayList typesToRemove = new ArrayList(); foreach (CodeTypeDeclaration codeType in codeNamespace.Types) { bool webDerived = false; foreach (CodeTypeReference baseType in codeType.BaseTypes) { if (baseType.BaseType == "System.Web.Services.Protocols.SoapHttpClientProtocol") { webDerived = true; break; } } // We can't remove elements from a collection while we're iterating over it... if (!webDerived) { typesToRemove.Add(codeType); } } // ...so we remove them later foreach (CodeTypeDeclaration codeType in typesToRemove) { codeNamespace.Types.Remove(codeType); } // Add the missing using statements. Should probably allow these to be specified // on the command line, but hardcode them for now foreach(string namespaceStr in par.UsingStatements) { codeNamespace.Imports.Add(new CodeNamespaceImport(namespaceStr)); } } } } }