Making WCF Output a single WSDL file for interop purposes.

Posted on Dot net Slackers See other posts from Dot net Slackers
Published on Tue, 16 Mar 2010 00:00:00 GMT Indexed on 2010/03/16 12:56 UTC
Read the original article Hit count: 1492

Filed under:

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
+ <;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 youll 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.

Did you know that DotNetSlackers also publishes .net articles written by top known .net Authors? We already have over 80 articles in several categories including Silverlight. Take a look: here.



Email this Article

© Dot net Slackers or respective owner

Making WCF Output a single WSDL file for interop purposes.

Posted by Glav on ASP.net Weblogs See other posts from ASP.net Weblogs or by Glav
Published on Tue, 16 Mar 2010 11:42:21 GMT Indexed on 2010/03/16 11:46 UTC
Read the original article Hit count: 1492

Filed under:
|
|
|
|

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.

© ASP.net Weblogs or respective owner

Related posts about ASP.NET

Related posts about .NET