Making WCF Output a single WSDL file for interop purposes.

Published Tuesday, March 16, 2010 10:42 PM

By default, when WCF emits a WSDL definition for your services, it can often contain many links to others related schemas that need to be imported. For the most part, this is fine. WCF clients understand this type of schema without issue, and it conforms to the requisite standards as far as WSDL definitions go.

However, some non Microsoft stacks will only work with a single WSDL file and require that all definitions for the service(s) (port types, messages, operation etc…) are contained within that single file. In other words, no external imports are supported. Some Java clients (to my working knowledge) have this limitation. This obviously presents a problem when trying to create services exposed for consumption and interop by these clients.

Note: You can download the full source code for this sample from here

To illustrate this point, lets say we have a simple service that looks like:

Service Contract

public interface IService1
{
    [OperationContract]
    [FaultContract(typeof(DataFault))]
    string GetData(DataModel1 model);

    [OperationContract]
    [FaultContract(typeof(DataFault))]
    string GetMoreData(DataModel2 model);
}

Service Implementation/Behaviour

public class Service1 : IService1
{
    public string GetData(DataModel1 model)
    {
        return string.Format("Some Field was: {0} and another field was {1}", model.SomeField,model.AnotherField);
    }
    public string GetMoreData(DataModel2 model)
    {
        return string.Format("Name: {0}, age: {1}", model.Name, model.Age);
    }
}

Configuration File

<system.serviceModel>
<services>
  <service name="SingleWSDL_WcfService.Service1" behaviorConfiguration="SingleWSDL_WcfService.Service1Behavior">
<!-- ...std/default data omitted for brevity..... -->
    <endpoint address ="" binding="wsHttpBinding" contract="SingleWSDL_WcfService.IService1" >
          .......
 </services>
      <behaviors>
      <serviceBehaviors>
        <behavior name="SingleWSDL_WcfService.Service1Behavior">
             ........
        </behavior>
      </serviceBehaviors>
    </behaviors>

</system.serviceModel>

When WCF is asked to produce a WSDL for this service, it will produce a file that looks something like this (note: some sections omitted for brevity):

 <?xml version="1.0" encoding="utf-8" ?> 
- <wsdl:definitions name="Service1" targetNamespace="http://tempuri.org/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"     ...... namespace definitions omitted for brevity
+ &lt;wsp:Policy wsu:Id="WSHttpBinding_IService1_policy">
      ... multiple policy items omitted for brevity
  </wsp:Policy>
- <wsdl:types>
- <xsd:schema targetNamespace="http://tempuri.org/Imports">
  <xsd:import schemaLocation="http://localhost:2370/HostingSite/Service-default.svc?xsd=xsd0" namespace="http://tempuri.org/" /> 
  <xsd:import schemaLocation="http://localhost:2370/HostingSite/Service-default.svc?xsd=xsd3" namespace="Http://SingleWSDL/Fault" /> 
  <xsd:import schemaLocation="http://localhost:2370/HostingSite/Service-default.svc?xsd=xsd1" namespace="http://schemas.microsoft.com/2003/10/Serialization/" /> 
  <xsd:import schemaLocation="http://localhost:2370/HostingSite/Service-default.svc?xsd=xsd2" namespace="http://SingleWSDL/Model1" /> 
  <xsd:import schemaLocation="http://localhost:2370/HostingSite/Service-default.svc?xsd=xsd4" namespace="http://SingleWSDL/Model2" /> 
  </xsd:schema>
  </wsdl:types>
+ <wsdl:message name="IService1_GetData_InputMessage">
      ....
  </wsdl:message>
- <wsdl:operation name="GetData">
     .....
  </wsdl:operation>
- <wsdl:service name="Service1">
     .......
  </wsdl:service>
  </wsdl:definitions>

The above snippet from the WSDL shows the external links and references that are generated by WCF for a relatively simple service. Note the xsd:import statements that reference external XSD definitions which are also generated by WCF.

In order to get WCF to produce a single WSDL file, we first need to follow some good practices when it comes to WCF service definitions.

Step 1: Define a namespace for your service contract.

[ServiceContract(Namespace="http://SingleWSDL/Service1")]
public interface IService1
{
       ......
}

Normally you would not use a literal string and may instead define a constant to use in your own application for the namespace.

When this is applied and we generate the WSDL, we get the following statement inserted into the document:

  <wsdl:import namespace="http://SingleWSDL/Service1" location="http://localhost:2370/HostingSite/Service-default.svc?wsdl=wsdl0" /> 

All the previous imports have gone. If we follow this link, we will see that the XSD imports are now in this external WSDL file. Not really any benefit for our purposes.

Step 2: Define a namespace for your service behaviour

[ServiceBehavior(Namespace = "http://SingleWSDL/Service1")]
public class Service1 : IService1
{
      ......
}

As you can see, the namespace of the service behaviour should be the same as the service contract interface to which it implements. Failure to do these tasks will cause WCF to emit its default http://tempuri.org namespace all over the place and cause WCF to still generate import statements. This is also true if the namespace of the contract and behaviour differ. If you define one and not the other, defaults kick in, and you’ll find extra imports generated.

While each of the previous 2 steps wont cause any less import statements to be generated, you will notice that namespace definitions within the WSDL have identical, well defined names.

Step 3: Define a binding namespace

In the configuration file, modify the endpoint configuration line item to iunclude a bindingNamespace attribute which is the same as that defined on the service behaviour and service contract

<endpoint 
    address="" 
    binding="wsHttpBinding" 
    contract="SingleWSDL_WcfService.IService1" 
    bindingNamespace="http://SingleWSDL/Service1">

However, this does not completely solve the issue. What this will do is remove the WSDL import statements like this one:

<wsdl:import namespace="http://SingleWSDL/Service1" 
location="http://localhost:2370/HostingSite/Service-default.svc?wsdl" /> 

from the generated WSDL.

Finally…. the magic….

Step 4: Use a custom endpoint behaviour to read in external imports and include in the main WSDL output.

In order to force WCF to output a single WSDL with all the required definitions, we need to define a custom WSDL Export extension that can be applied to any endpoints. This requires implementing the IWsdlExportExtension and IEndpointBehavior interfaces and then reading in any imported schemas, and adding that output to the main, flattened WSDL to be output. Sounds like fun right…..? Hmmm well maybe not.

This step sounds a little hairy, but its actually quite easy thanks to some kind individuals who have already done this for us.

As far as I know, there are 2 available implementations that we can easily use to perform the import and “WSDL flattening”.  WCFExtras which is on codeplex and FlatWsdl by Thinktecture. Both implementations actually do exactly the same thing with the imports and provide an endpoint behaviour, however FlatWsdl does a little more work for us by providing a ServiceHostFactory that we can use which automatically attaches the requisite behaviour to our endpoints for us.

To use this in an IIS hosted service, we can modify the .SVC file to specify this ne factory to use like so:

<%@ ServiceHost Language="C#" 
          Debug="true" 
          Service="SingleWSDL_WcfService.Service1" 
          Factory="Thinktecture.ServiceModel.Extensions.Description.FlatWsdlServiceHostFactory"  %>

Within a service application or another form of executable such as a console app, we can simply create an instance of the custom service host and open it as we normally would as shown here:

FlatWsdlServiceHost host = new FlatWsdlServiceHost(typeof(Service1));
host.Open();

And we are done. WCF will now generate one single WSDL file that contains all he WSDL imports and data/XSD imports.

You can download the full source code for this sample from here

Hope this has helped you.

Note: Please note that I have not extensively tested this in a number of different scenarios so no guarantees there.

by Glav
Filed under: , , , ,

Comments

# rstrahl said on Tuesday, March 16, 2010 5:47 PM

Thanks Glav for putting all this into one place. Much appreciated...

# Vijay said on Wednesday, July 21, 2010 9:07 AM

Really helped a lot.

Thanks

# bvinodkumar1977 said on Saturday, March 26, 2011 5:20 AM

I have a different question related to your post. The post is really very easy to understand. Thanks for that.

I have a built a WCF service and used in web application (.net). I used service reference provided in VS2008 for referencing the service in client. The app worked fine in DEV and in QA. It didnot work in PROD. I see that by adding reference to services I have XSDs in separate files and wsdl imports them. Why it worked in QA and not in Production? Is it because some thing is not installed properly in production? Please help.

# bvinodkumar1977 said on Saturday, March 26, 2011 5:21 AM

I forgot to mention the error:

parse error message: unable to import binding wshttpbinding from namespace tempuri.com

# Rudra said on Friday, July 8, 2011 3:20 AM

The FaltWSDL dll works fine if your WCF service contains only endpoints using a single binding. If the service has multiple endpoints defined with different bindings (basicHttpBinding and wsHttpBinding) then the FlatWSDL implementation fails because the servicehostfactory forces the flatwsdl factory to import the schemas for each endpoint binding.This can be avoided by either or configuring the servicehostfactory inline or modifying the Flatwsdl code so that already imported schemas are not reimported.

# Ed said on Tuesday, July 19, 2011 6:09 PM

Great, thanks!  

Everything works well, however, the "machine" name appears in my WSDL.  I need my IP address to appear instead.  Also, I cannot modify IIS to achieve this, therefore, is there something in the config or service application, which I can change to force the IP address to appear throughout my WSDL?

Thank You!

# pramodh said on Thursday, November 10, 2011 8:23 AM

How to modify Flatwsdl code to support multiple bindings?

Leave a Comment

(required) 
(required) 
(optional)
(required) 

This Blog

Syndication