Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

WSDL Imports without WSDL.exe


:P
On this page:

[updated 2/16/2009: Added support for imported schemas so WCF services can work]

A couple of weeks back I have been working on a Web Service client tool for COM clients. With the SOAP Toolkit DOA one of the things that old (for me most FoxPro apps of clients) apps need to do is access Web Services and .NET is really become the only viable option if calling a complex service is required. I’ve been swamped with work in this area recently (which isn’t a bad thing) but clearly a lot of people are finding they need to use Web services with FoxPro or other older COM based technologies.

Anyway – the tool I built basically wraps WSDL.exe, plus compilation into an assembly, plus a client proxy class (in FoxPro in this case) generator that creates a native fox class to talk to the .NET component.

One issue that I ran into my original implementation is that WSDL.exe is required to import Web Services. Either you end up using the Visual Studio tools which create Web References or Service References (WCF) or you can use WSDL.exe or SvcUtil.exe both of which are not actually part of the base .NET distribution, but only part of the separately installed Windows/.NET SDKs. This means that in a lot of cases the EXEs might not even be present. My only option then really would have been to ship WSDL.exe, which isn’t legal – or tell people to download the SDK (which isn’t small).

So, the other day somebody – Kerem Kusmezer -  left a comment on one of my earlier posts and posted some code that effectively duplicates a good chunk of the functionality that WSDL.exe provides. I ended up modifying the code a little and adding support for compilation of the generated proxy into an assembly ready for dynamic loading or as is the case for my tool to use as a base for creating the FoxPro proxy class.

The overall process is surprising easy once you have a general idea what classes are involved. Here’s what I ended up with to handle proxy class generation and compilation of the code into an assembly (again hat tip to Kerem’s message and also this link which gives a step by step explanation of the steps involved):

   /// <summary>
   /// WSDL Parser class that is responsible for:   
   /// Creating a .cs code file
   /// Compiling the .cs code file into an Assembly
   /// Parsing the WSDL generated class into a class structure consumable by FoxPro
   /// (since Reflection objects are not COM friendly)
   /// </summary>
    [ClassInterface(ClassInterfaceType.AutoDual)]    
    public class WsdlClassParser : MarshalByRefObject
    {
        public string Assembly = null;
        public AppDomain LocalAppDomain = null;
        public string ErrorMessage = "";

        /// <summary>
        /// This function basically reproduces the functionality that WSDL.exe provides and generates
        /// a CSharp class that is a proxy to the Web service specified at the provided WSDL URL.
        /// </summary>
        /// <param name="wsdlUrl"></param>
        /// <param name="generatedSourceFilename"></param>
        /// <param name="generatedNamespace"></param>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public bool GenerateWsdlProxyClass(string wsdlUrl, string generatedSourceFilename, string generatedNamespace, string username, string password)
        {
            // erase the source file
            if (File.Exists(generatedSourceFilename))
                File.Delete(generatedSourceFilename);            
            
            // download the WSDL content into a service description
            WebClient http = new WebClient();
            ServiceDescription sd = null;
                        
            if (!string.IsNullOrEmpty(username))
                http.Credentials = new NetworkCredential(username, password);

            try
            {                                                
                sd = ServiceDescription.Read( http.OpenRead(wsdlUrl));                
            }
            catch (Exception ex)
            {
                this.ErrorMessage = "Wsdl Download failed: " + ex.Message;
                return false;
            }
            
            // create an importer and associate with the ServiceDescription
            ServiceDescriptionImporter importer = new ServiceDescriptionImporter();
            importer.ProtocolName = "SOAP";            
            importer.CodeGenerationOptions = CodeGenerationOptions.None;            
            importer.AddServiceDescription(sd, null, null);
            
            // Download and inject any imported schemas (ie. WCF generated WSDL)            
            foreach (XmlSchema wsdlSchema in sd.Types.Schemas)
            {
                // Loop through all detected imports in the main schema
                foreach (XmlSchemaObject externalSchema in wsdlSchema.Includes)
                {
                    // Read each external schema into a schema object and add to importer
                    if (externalSchema is XmlSchemaImport)
                    {
                        Uri baseUri = new Uri(wsdlUrl);
                        Uri schemaUri = new Uri(baseUri, ((XmlSchemaExternal)externalSchema).SchemaLocation);
                        
                        Stream schemaStream = http.OpenRead(schemaUri);
                        System.Xml.Schema.XmlSchema schema = XmlSchema.Read(schemaStream, null);
                        importer.Schemas.Add(schema);
                    }
                }
            }

            // set up for code generation by creating a namespace and adding to importer
            CodeNamespace ns = new CodeNamespace(generatedNamespace);
            CodeCompileUnit ccu = new CodeCompileUnit();
            ccu.Namespaces.Add(ns);
            importer.Import(ns, ccu);

            // final code generation in specified language
            CSharpCodeProvider provider = new CSharpCodeProvider();
            IndentedTextWriter tw = new IndentedTextWriter(new StreamWriter(generatedSourceFilename));
            provider.GenerateCodeFromCompileUnit(ccu, tw, new CodeGeneratorOptions());

            tw.Close();

            return File.Exists(generatedSourceFilename);
        }

  
        /// <summary>
        /// Compiles the 
        /// </summary>
        /// <param name="sourceFile"></param>
        /// <param name="targetAssembly"></param>
        /// <returns></returns>
        public bool CompileSource(string sourceFile, string targetAssembly)
        {
            // delete existing assembly first 
            if (File.Exists(targetAssembly))
            {
                try
                {
                    // this might fail if assembly is in use 
                    File.Delete(targetAssembly);
                }
                catch (Exception ex)
                {                   
                    this.ErrorMessage = ex.Message;
                    return false;
                }            
            }

            // read the C# source file
            StreamReader sr = new StreamReader(sourceFile);
            string fileContent = sr.ReadToEnd();
            sr.Close();

            // Embed COM visibility into code so Intellisense works on client
            fileContent = StringUtils.ReplaceString(fileContent, "namespace ",
@"
    // West Wind DotNetWsdlGenerator inserted to allow for COM registration
    using System.Runtime.InteropServices;
    [assembly: ComVisible(true)]
    [assembly: ClassInterface(ClassInterfaceType.AutoDual)]

    namespace ",false);

            // Write the modified file back to disk
            StreamWriter sw = new StreamWriter(sourceFile);
            sw.Write(fileContent);
            sw.Close();

            // set up compiler and add required references
            ICodeCompiler compiler = new CSharpCodeProvider().CreateCompiler();
            CompilerParameters parameter = new CompilerParameters();
            parameter.OutputAssembly = targetAssembly;
            parameter.ReferencedAssemblies.Add("System.dll");
            parameter.ReferencedAssemblies.Add("System.Web.Services.dll");
            parameter.ReferencedAssemblies.Add("System.Xml.dll");

            // *** support DataSet/DataTable results
            parameter.ReferencedAssemblies.Add("System.Data.dll");
            
            // Do it: Final compilation to disk
            CompilerResults results = compiler.CompileAssemblyFromFile(parameter,sourceFile);

            if (File.Exists(targetAssembly))
                return true;
            
            // flatten the compiler error messages into a single string
            foreach (CompilerError err in results.Errors)
            {
                this.ErrorMessage += err.ToString() + "\r\n";
            }
            
            return false;
        }
        … additional methods for assembly type information omitted
}

The key is the GenerateWsdlProxy method which roughly performs the task that WSDL.exe performs on a WSDL import. Most of the options you get from the command line are also available on ServiceDescription or vai

The steps to this revolve around the ServiceDescriptionImporter class which performs the importing tasks of the WSDL and can output the results into a CodeDOM object for code generation. There’s also a ServiceDescription class which provides a high level representation of the WSDL document in a frustratingly limited fashion. The two objects in combination together with a CodeDOM object allow for code generation with relatively little (although rather undiscoverable) code. You can set a host of options on the import process which map somewhat closely to the options provided by WSDL.exe. In the code above I set up specifcally for WSDL importing and client proxy generation and specifically generating just the actual mapping proxy types (by using CodeGenerationOptions.None) which is appropriate for the tasks I need to perform in my tool. For more complete support of WSDL features you’d probably need a few additional parameters or class properties to determine behavior.

The CompileSource method then allows compilation of the generated proxy class into an assembly. Luckily compiling code in .NET is very easy and can be done with just a few lines of code. You can then use this generated class dynamically or as is the case in my particular situation use the generated assembly to parse through the generated types and create a proxy for a non-.NET environment.

This is definitely not something that one needs to use very often, but it’s good to have a self contained solution to creating a Web Service proxy like this. I have 2 different applications that use this: The above mentioned proxy generator and a WSDL documentation tool that reads the core structure of the service and then manually picks up only some of the information that the proxy doesn’t publish directly (like descriptions for example).

One (not so small) Gotcha: WCF and those damn WCF External Schemas

On fly in the ointment is Windows Communication Foundation which has the unfortunate habit to create the WSDL XML with external <xsd:import> tags to handle message types. All of the support message schemas  are stored in external links – xsd:import - which looks like this:

<wsdl:types>
   <xsd:schema targetNamespace="http://tempuri.org/Imports">
    <xsd:import schemaLocation="http://rasnote/WcfService1/Service1.svc?xsd=xsd0" namespace="http://tempuri.org/" />
    <xsd:import schemaLocation="http://rasnote/WcfService1/Service1.svc?xsd=xsd1" namespace="http://schemas.microsoft.com/2003/10/Serialization/" />
    <xsd:import schemaLocation="http://rasnote/WcfService1/Service1.svc?xsd=xsd2" namespace="http://schemas.datacontract.org/2004/07/WcfService1" />
  </xsd:schema>
</wsdl:types>

A plain import into ServiceDescript.Read() isn’t going to automatically import the external schemas. I wasted quite a bit of time trying to find a solution to this particular issue, but it looks like natively the various .NET Xml objects do not import external schemas which is kind of lame. After a lot of searching and running down dead end blind alleys I finally found a thread on the ASP.NET forums that demonstrates how to pull in the imported schemas into a service description:

importer.AddServiceDescription(sd, null, null);

// Download and inject any imported schemas (ie. WCF generated WSDL)            
foreach (XmlSchema wsdlSchema in sd.Types.Schemas)
{
    // Loop through all detected imports in the main schema
    foreach (XmlSchemaObject externalSchema in wsdlSchema.Includes)
    {
        // Read each external schema into a schema object and add to importer
        if (externalSchema is XmlSchemaImport)
        {
            Uri baseUri = new Uri(wsdlUrl);
            Uri schemaUri = new Uri(baseUri, ((XmlSchemaExternal)externalSchema).SchemaLocation);
            
            Stream schemaStream = http.OpenRead(schemaUri);
            System.Xml.Schema.XmlSchema schema = XmlSchema.Read(schemaStream, null);
            importer.Schemas.Add(schema);
        }
    }
}

This code basically loops through the imported serviceDescription’s main schema, picks up any included types and then explicitly imports and attaches them to the importer. It’s not terribly complex how this works, but the trick is in finding the right objects to work with.

The above code works to make WCF services work at least and so this solves the main usage scenario for Web Service imports in my situation.

Hopefully some of you find this helpful – I know it took a bit for me to find this particular solution hidden in a message on forums.

Posted in Web Services  XML  

The Voices of Reason


 

Christian Weyer
February 12, 2009

# re: WSDL Imports without WSDL.exe

Feel free to sneak into the WSCF code.
WSCF.classic (ASMX): http://www.codeplex.com/WSCFclassic
WSCF.blue (WCF): http://www.codeplex.com/WSCFblue

-Christian

Christopher Steen
February 13, 2009

# Link Listing - February 12, 2009

Link Listing - February 12, 2009

Kevin Dente
February 15, 2009

# re: WSDL Imports without WSDL.exe

You might try looking at the source for Web Service Studio - I think it handles this case:
http://www.codeplex.com/WebserviceStudio

Rick Strahl
February 15, 2009

# re: WSDL Imports without WSDL.exe

Thanks @Kevin and @Christian - Looking into WSCF code I found a couple of things that pointed me in the right direction for manual schema importing, and it now works.

I've updated the post above with import code.

Paul Corriveau
February 18, 2009

# re: WSDL Imports without WSDL.exe

Thanks for the code, Rick.

There's another 'Gotcha' - you also have to take care of additional wsdl definitions pulled in via the import route. The import should look something like this:

<wsdl:import namespace="http://www.companyx.com/ns/Billing/externalWsdl" location="http://xsrvr/BillingWebServices/PremiumBill/AccountHistory.asmx?wsdl=wsdl1"/>

Handle it just like you did with the external schemas:

foreach (Import imp in sd.Imports)
{
Uri importsUri = new Uri(wsdlUrl, imp.Location);
// get the ServiceDescription and add it to the ServiceDescriptionImporter
}

Erik
June 24, 2009

# re: WSDL Imports without WSDL.exe

Speaking of WCF Services, you might find this tool helpful. It basically does what you have described in your blog post (plus other neat features)

Anyway, here it is. http://www.wcfstorm.com

charlie
June 21, 2012

# re: WSDL Imports without WSDL.exe

i need help about the code , i´m trying to import xsd schemes but my wsdl doesnt have schemelocation,what can i do??

- <wsdl:types>
- <xs:schema xmlns:ax22="http://service.logic.svr.test.us/xsd" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://interfaces.ws.svr.test.us">
<xs:import namespace="http://service.logic.svr.test.us/xsd" />

Alexander
October 03, 2014

# re: WSDL Imports without WSDL.exe

this code don't work with this:
http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl
however it work with the wsdl.exe tool
I trying to solve this for several days. Please any help will be significant.

Raja Nadar
May 10, 2015

# re: WSDL Imports without WSDL.exe

Hey Rick

excellent post. was very helpful, especially the external-schema-include part.

one thing i would like to add is that if the external schema contains, additionally referenced external types, then the code generation fails. the solution is to recursively add the external-schemas. (and ensure there are no loops by having an uber HashSet<string> of Schema Uris)

your post was super helpful, and hope this additional tip helps somebody with
nested schema-includes.

sample code:

// Download and inject any imported schemas (ie. WCF generated WSDL)            
foreach (XmlSchema wsdlSchema in sd.Types.Schemas)
{
    // Loop through all detected imports in the main schema
    ImportIncludedSchemasRecursively(wsdlUrl, importer, wsdlSchema);
}
 
// later code
 
// class level variable
private static readonly HashSet<string> IncludedSchemas = new HashSet<string>();
 
private static void ImportIncludedSchemasRecursively(string mainWsdlUrl, ServiceDescriptionImporter importer, XmlSchema currentWsdlSchema)
{
    foreach (XmlSchemaObject externalSchema in currentWsdlSchema.Includes)
    {
        // Read each external schema into a schema object and add to importer
        if (externalSchema is XmlSchemaImport)
        {
            Uri baseUri = new Uri(mainWsdlUrl);
            Uri schemaUri = new Uri(baseUri, ((XmlSchemaExternal)externalSchema).SchemaLocation);
 
            if (!IncludedSchemas.Contains(schemaUri.ToString()))
            {
                IncludedSchemas.Add(schemaUri.ToString());
 
                WebClient http = new WebClient();
                Stream schemaStream = http.OpenRead(schemaUri);
 
                System.Xml.Schema.XmlSchema schema = XmlSchema.Read(schemaStream, null);
                importer.Schemas.Add(schema);
 
                ImportIncludedSchemasRecursively(mainWsdlUrl.ToString(), importer, schema);
            }
        }
    }
}

Tolga
December 16, 2015

# re: WSDL Imports without WSDL.exe

One of the very few posts worked for me, great thanks!

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024