[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.
Other Posts you might also like