Cannot get latest BizTalk Services SDK (May 2) working?
Problem with svcutil.exe in Orcas Beta 2

Improving WCF Interoperability: Flattening your WSDL


WCF is surely a big step forward for having a unified communication runtime, API and hosting model. And with all these super-exciting specifications and standards supported WCF must be a killer when it comes to interoperability with other platforms, technologies, stacks. And now the communication gurus from Redmond even announced that they will support everything to imagine (OK, except CORBA, ICE and Remoting wire interop ;)) under the big communication umbrella - including popular rockstars like REST-style architectures, RSS- and Atom-driven feeds and JSON for the AJAXians. Oh, and of course they still love SOAP. You know: love is the message and the message is love. So life is good, right?

Well, yeah... in theory.

Back to real life.

I was approached by customers recently. Each of them had to interop with some issues when trying to point there 'other' technologies to a WCF service. And yes, this service was a plain old 'Web Service' - i.e. basicHttpBinding, no security ;), using the default DataContractSerializer with no fancy versioning or even extensibility stuff. Well, something pretty straight-forward at the end of the day.

But these other technologies - namely Flex and PHP - could just not get their code generated (statically or dynamically) from the metadata exposed by the basic and simple WCF service. Hm... it is actually neither's fault, maybe we could blame Microsoft that they are ahead of the rest of the world - maybe we could blame the others that they have no time to invest in improving supporting WSDL and XSD on their platforms. But in the end, nobody is to blame, nobody has to complain: we simply need a solution. Period.

It turned out that both technologies struggled with the way WCF is exposing the WSDL and XSD metadata by default. IMHO, WCF does a good job in factoring the XSD and WSDLs into multiple parts and not pumping everything into one big WSDL document. But the others just do not like it. Take a look at the following WSDL from our popular RestaurantService sample, just ported over to WCF  (parts removed for clarity):

- <wsdl:definitions name="RestaurantService" targetNamespace="http://www.thinktecture.com/services/restaurant/2006/12" ...>
- <wsdl:types>
- <xsd:schema targetNamespace="http://www.thinktecture.com/services/restaurant/2006/12/Imports">
  <xsd:import schemaLocation="http://localhost:7777/WSCF?xsd=xsd0" namespace="http://www.thinktecture.com/services/restaurant/2006/12" />
  <xsd:import schemaLocation="http://localhost:7777/WSCF?xsd=xsd1" namespace="http://schemas.microsoft.com/2003/10/Serialization/" />
  <xsd:import schemaLocation="http://localhost:7777/WSCF?xsd=xsd2" namespace="urn:thinktecture-com:demos:restaurantservice:messages:v1" />
  <xsd:import schemaLocation="http://localhost:7777/WSCF?xsd=xsd3" namespace="urn:thinktecture-com:demos:restaurantservice:data:v1" />
  </xsd:schema>
  </wsdl:types>
...
  </wsdl:definitions>


Other platforms just do not like this kind of factoring out the XSDs into external, although virtual, locations. It gets even worse when you do not remember to set all necessary namespace values. Kirill wrote about this some time ago and my friend Beat and I were also struggling with this a along time ago, It is not very obvious in the first place which properties you need to see and especially why. But once you accept it, this is how it goes:

<wsdl:definitions>  è     [ServiceBehavior]
<wsdl:portType>  è [ServiceContract]
<wsdl:part>  è [MessageContract]
<xs:schema>  è [DataContract]
    [MessageContract]
    [MessageBodyMember]
<binding>  è bindingNamespace on endpoint

With a namespace value being present in [ServiceContract], [ServiceBehavior] and bindingNamespace we are good to go and will no longer see that WCF imports a wsdl1 into the root WSDL - essentially, the WSDL shown above results from already setting these values correctly.

OK. Done? Nope.

We still have these xsd:import statements which so many other aliens are not happy with. We need to get rid of them. Tomas already talked about the basic idea the other day, but I needed to tweak it a bit and further enhance it.
We need to hook into the WSDL generation process inside WCF and rip out the XSD imports and place all that stuff straight into the schema section of the WSDL.

The idea is to implement the IWsdlExportExtension interface in a class called FlatWsdl in order to get our fingers onto the metadata. We attach this extension to the WCF runtime by having an endpoint behavior. When hosting a service in your own application you can just add the endpoint behavior to your appropriate endpoint(s) like this:

ServiceHost sh = new ServiceHost(

   typeof(RestaurantService),

   new Uri("http://localhost:7777/WSCF"));

 

foreach(ServiceEndpoint endpoint in sh.Description.Endpoints)

   endpoint.Behaviors.Add(new FlatWsdl());

When now pointing a browser to the WSDL, we see what we wanted to see (parts are omitted for brevity):

- <wsdl:definitions name="RestaurantService" targetNamespace="http://www.thinktecture.com/services/restaurant/2006/12" ...>
- <wsdl:types>
- <xsd:schema elementFormDefault="qualified" targetNamespace="http://www.thinktecture.com/services/restaurant/2006/12">
- <xsd:element name="RateRestaurant">
- <xsd:complexType>
- <xsd:sequence>
  <xsd:element minOccurs="0" name="message" nillable="true" type="q1:RateRestaurantMessage" xmlns:q1="urn:thinktecture-com:demos:restaurantservice:messages:v1" />
  </xsd:sequence>
  </xsd:complexType>
  </xsd:element>
- <xsd:element name="RateRestaurantResponse">
- <xsd:complexType>
  <xsd:sequence />
  </xsd:complexType>
  </xsd:element>
- <xsd:element name="ListRestaurants">
- <xsd:complexType>
- <xsd:sequence>
  <xsd:element minOccurs="0" name="message" nillable="true" type="q2:GetRestaurantsRequest" xmlns:q2="urn:thinktecture-com:demos:restaurantservice:messages:v1" />
  </xsd:sequence>
  </xsd:complexType>
  </xsd:element>
- <xsd:element name="ListRestaurantsResponse">
- <xsd:complexType>
- <xsd:sequence>
  <xsd:element minOccurs="0" name="ListRestaurantsResult" nillable="true" type="q3:GetRestaurantsResponse" xmlns:q3="urn:thinktecture-com:demos:restaurantservice:messages:v1" />
  </xsd:sequence>
  </xsd:complexType>
  </xsd:element>
  </xsd:schema>
- <xsd:schema elementFormDefault="qualified" targetNamespace="urn:thinktecture-com:demos:restaurantservice:messages:v1" xmlns:tns="urn:thinktecture-com:demos:restaurantservice:messages:v1">
- <xsd:complexType name="RateRestaurantMessage">
- <xsd:sequence>
  <xsd:element name="restaurantID" type="xsd:int" />
  <xsd:element name="rate" type="q4:RatingInfo" xmlns:q4="urn:thinktecture-com:demos:restaurantservice:data:v1" />
  </xsd:sequence>
  </xsd:complexType>
  <xsd:element name="RateRestaurantMessage" nillable="true" type="tns:RateRestaurantMessage" />
- <xsd:complexType name="GetRestaurantsRequest">
- <xsd:sequence>
- <xsd:element name="zip" nillable="true" type="xsd:string">
- <xsd:annotation>
- <xsd:appinfo>
  <DefaultValue EmitDefaultValue="false" xmlns="http://schemas.microsoft.com/2003/10/Serialization/" />
  </xsd:appinfo>
  </xsd:annotation>
  </xsd:element>
  </xsd:sequence>
  </xsd:complexType>
  <xsd:element name="GetRestaurantsRequest" nillable="true" type="tns:GetRestaurantsRequest" />
- <xsd:complexType name="GetRestaurantsResponse">
- <xsd:sequence>
- <xsd:element name="restaurants" nillable="true" type="q5:RestaurantsList" xmlns:q5="urn:thinktecture-com:demos:restaurantservice:data:v1">
- <xsd:annotation>
- <xsd:appinfo>
  <DefaultValue EmitDefaultValue="false" xmlns="http://schemas.microsoft.com/2003/10/Serialization/" />
  </xsd:appinfo>
  </xsd:annotation>
  </xsd:element>
  </xsd:sequence>
  </xsd:complexType>
  <xsd:element name="GetRestaurantsResponse" nillable="true" type="tns:GetRestaurantsResponse" />
  </xsd:schema>
- <xsd:schema elementFormDefault="qualified" targetNamespace="urn:thinktecture-com:demos:restaurantservice:data:v1" xmlns:tns="urn:thinktecture-com:demos:restaurantservice:data:v1">
- <xsd:simpleType name="RatingInfo">
- <xsd:restriction base="xsd:string">
  <xsd:enumeration value="poor" />
  <xsd:enumeration value="good" />
  <xsd:enumeration value="veryGood" />
  <xsd:enumeration value="excellent" />
  </xsd:restriction>
  </xsd:simpleType>
  <xsd:element name="RatingInfo" nillable="true" type="tns:RatingInfo" />
- <xsd:complexType name="RestaurantsList">
- <xsd:sequence>
  <xsd:element minOccurs="0" maxOccurs="unbounded" name="restaurant" nillable="true" type="tns:RestaurantInfo" />
  </xsd:sequence>
  </xsd:complexType>
  <xsd:element name="RestaurantsList" nillable="true" type="tns:RestaurantsList" />
- <xsd:complexType name="RestaurantInfo">
- <xsd:sequence>
  <xsd:element name="restaurantID" type="xsd:int" />
- <xsd:element name="name" nillable="true" type="xsd:string">
- <xsd:annotation>
- <xsd:appinfo>
  <DefaultValue EmitDefaultValue="false" xmlns="http://schemas.microsoft.com/2003/10/Serialization/" />
  </xsd:appinfo>
  </xsd:annotation>
  </xsd:element>
- <xsd:element name="address" nillable="true" type="xsd:string">
- <xsd:annotation>
- <xsd:appinfo>
  <DefaultValue EmitDefaultValue="false" xmlns="http://schemas.microsoft.com/2003/10/Serialization/" />
  </xsd:appinfo>
  </xsd:annotation>
  </xsd:element>
- <xsd:element name="city" nillable="true" type="xsd:string">
- <xsd:annotation>
- <xsd:appinfo>
  <DefaultValue EmitDefaultValue="false" xmlns="http://schemas.microsoft.com/2003/10/Serialization/" />
  </xsd:appinfo>
  </xsd:annotation>
  </xsd:element>
- <xsd:element name="state" nillable="true" type="xsd:string">
- <xsd:annotation>
- <xsd:appinfo>
  <DefaultValue EmitDefaultValue="false" xmlns="http://schemas.microsoft.com/2003/10/Serialization/" />
  </xsd:appinfo>
  </xsd:annotation>
  </xsd:element>
- <xsd:element name="zip" nillable="true" type="xsd:string">
- <xsd:annotation>
- <xsd:appinfo>
  <DefaultValue EmitDefaultValue="false" xmlns="http://schemas.microsoft.com/2003/10/Serialization/" />
  </xsd:appinfo>
  </xsd:annotation>
  </xsd:element>
- <xsd:element name="openFrom" nillable="true" type="xsd:string">
- <xsd:annotation>
- <xsd:appinfo>
  <DefaultValue EmitDefaultValue="false" xmlns="http://schemas.microsoft.com/2003/10/Serialization/" />
  </xsd:appinfo>
  </xsd:annotation>
  </xsd:element>
- <xsd:element name="openTo" nillable="true" type="xsd:string">
- <xsd:annotation>
- <xsd:appinfo>
  <DefaultValue EmitDefaultValue="false" xmlns="http://schemas.microsoft.com/2003/10/Serialization/" />
  </xsd:appinfo>
  </xsd:annotation>
  </xsd:element>
  </xsd:sequence>
  </xsd:complexType>
  <xsd:element name="RestaurantInfo" nillable="true" type="tns:RestaurantInfo" />
  </xsd:schema>
...
  </wsdl:types>
...
  </wsdl:definitions>

Yes, that's it.
But still I was not completely happy with my solution. Do I really always need to add this behavior to my endpoints? No, let's look at creating a custom ServiceHost that does the (kind of) heavy-lifting for us.

public class FlatWsdlServiceHost : ServiceHost

{

    public FlatWsdlServiceHost()

    {

    }

 

    public FlatWsdlServiceHost(Type serviceType, params Uri[] baseAddresses)

        :

        base(serviceType, baseAddresses)

    {

    }

 

    public FlatWsdlServiceHost(object singeltonInstance, params Uri[] baseAddresses)

        :

        base(singeltonInstance, baseAddresses)

    {

    }

 

    protected override void ApplyConfiguration()

    {

        base.ApplyConfiguration();

        InjectFlatWsdlExtension();

    }

 

    private void InjectFlatWsdlExtension()

    {

        foreach (ServiceEndpoint endpoint in this.Description.Endpoints)

        {

            endpoint.Behaviors.Add(new FlatWsdl());

        }

    }

}

Whew - that was easy. Thanks to Steve and friends for making it such easy.
Now my service hosting code boils down to this, which is really sexy:

FlatWsdlServiceHost sh = new FlatWsdlServiceHost(

   typeof(RestaurantService),

   new Uri("http://localhost:7777/WSCF"));

OK, now we are done!

No. What about web-hosting this service? What if I want to host this service inside of IIS and/or W(P)AS? Gladly, the good guys in Redmond provide us with a way to hook into the web-hosted activation, as well. ServiceHostFactory to the rescue! With a custom factory we can instruct the web host to use this one - and then we are on the safe side to just return our just implemented FlatWsdlServiceHost. The .svc hook then looks like this:

<% @ServiceHost Language=C# Debug="true"

   Factory="Thinktecture.ServiceModel.Extensions.Description.FlatWsdlServiceHostFactory"

   Service="Service.RestaurantService" %>

And for the sake of completeness, this is the simple factory:

public sealed class FlatWsdlServiceHostFactory : ServiceHostFactory

{

    public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)

    {

        return base.CreateServiceHost(constructorString, baseAddresses);

    }

 

    protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)

    {

        return new FlatWsdlServiceHost(serviceType, baseAddresses);

    }

}

This means that we can use the FlatWsdl extension in both. self- and web-hosted scenarios with close-to-zero intrusion from a programming model perspective. Sweet.
Wonderful - now I am satisfied.

Conclusion: WCF is extremely extensible - and this extensibility has helped me out already several dozens of times.
But then, WCF also makes the life of the common developer not always easier when caompared to the 'outdated' stacks - in fact, in some very popular situations and scenarios it may be a bit of a pain. You just need to know how to fix it.

Needless to say that my two customers where very happy. They could now easily point their codegen tools of choice to the WSDL  and generate code and afterwards successfully talk to the WCF service (talking is a totally different thing, BTW).
Please feel free to download the complete sample and give some feedback on it. Thanks.

P.S.: In homage to FlatEric (dedel-dede-dedem---dedel-dede-dedem...) I am calling this extension FlatWsdl.


Comments

Feed You can follow this conversation by subscribing to the comment feed for this post.

Micha#235;l Hompus

This works like a charm if you have only one endpoint. But if I have multiple endpoints the types node will get empty.

Christian Weyer

@ Michael Hompus: Yes, I have an improved version which spits out an exception if there are some 'circumstances' in which there simply cannot be produced a flat WSDL.

LOBOMINATOR

Hello Actually I don't really understand the problem here. The basicHttpBinding is fully ws-i compliant to the basic profile 1.1. I checked it myself with soapUI and the ws-i testing tools (http://www.ws-i.org). The schema referencing (therefore not a flatfile) is also defined by the standard and should be understood by soa technogolies which claim to be interoperable... Therefore I would never violate my WSDL with the FlatFileEndpoint...

Ronan Egan

Great example. Now I'm trying to get axis to consume a wsdl when I use wsHttpBinding... I'm currently weighing up if it's best to use WCF or your excellent WSCF project to build a WS for consumption by java clients, any thoughts yourself?

Mark J. Easton

What an excellent example of how to solve a common WCF problem. It's articles like this that make blogs such a useful resource. Good work. M

Ron Cordell

Came across this today in while trying to help a client consume WCF WSDL with their Java stack. Awesome article, many many thanks!

Martin Oss

Thank you very much for the FlatWsdl example. Unfortunately, when i try to generate a proxy using collection type Generic.List, the setting will be ignored, i always get Arrays. Do you have an idea how to fix that? Thanks, Martin.

Christian Weyer

Please get in touch with me via the Contact form and leave your email - we will sort this out.

Tobias Manthey

To use FlatWSDL in a IIS based WebServices a few lines of code must be added. See http://my-tech-talk.blogspot.com/2008/07/adding-flatwsdl-to-wcf-webservice.html

Willem

Thanks C, this looks great. But what should the contents of the FlatWSDL class be?

Graham MacDonald

I'm attempting to use this approach to overwrite the published endpoint address for my service. The service is hosted on machine behind a load balancer and I need to change both the address and port (address can be managed by host headers but I can't find a solution for the port). Is the address contained in the exporter's GeneratedWsdlDocuments, and if so, can you point me to it? Thanks!

Jonas

Has helped a lot: we have a customer who needs one file only, and he needs (needed thanks to you) it ASAP.

HJS

Is there an update available that supports multiple endpoints for IIS based hosting?

Inder Singh

Thanks for such a goodt post, I was able to get XSD schema generated inline with the code you provided but I had a hard time getting rid of wsdl:import../ statement. I kept getting two WSDL files with the second WSDL file containig XSD schema inline. I have tried to document the details on my blog post: http://isinghblog.blogspot.com/2009/03/wcf-service-and-adobe-livecycle_10.html

Christian Weyer

Please make sure that the ServiceContract namespace, the ServiceBehavior namespace and the binding namespace have the exact same value.

Christian Weyer

@HJS: please get in touch with me via the Contact from - thx!

Shrike

Hi. It's great but seems to be not fully correct. I start using this extension for my services and run into fail on getting wsdl. Getting wsdl for the first service always works, but for the next fails due to types dublication.

John Xu

There is an incident occurred to my client that the WCF interface generate a recursive reference. WSDL imports a XSD and the XSD points to this WSDL URL.

I am wondering in which version of the VS that this issue got addressed and user can chosse by web.config that can demand no xsd imported from WSDL? Also, import XSD in WSDL may cause problem in enterprise environment because of firewall if WSDl has import attributes to XSD.

Christian Weyer

Hi John,

are you talking about a problem using FlatWsdl or just complain about Visual Studio? :)
If you can give me more details I would be happy to help out.

Cheers,
Christian

Brian

After implementing the flat wsdl design. Svcutil.exe now generates string instead of Guid, and Duration. Which was not the case with the wsdl containing the import's. Any ideas why this is so?

TY for a great post.

Christian Weyer

Brian, can you get in touch with me via email? I think I may have an idea... Thanks!

Simon

I have a problem that I think this would help with: I'm implementing a service using the Declarative WCF Workflow Service in .NET 4.0 beta 2. I'm struggling to work out how to implement this, given that I have no code - it's all in a .xamlx file. Have you any experience with this?

Christian Weyer

Hi Simon,

yes, this is unfortuantely "the way to go" in WF4 for now. It is all implicit contracts in XAML, if you will...
The WF team is working on a story and some tooling to enable a better contract-first experience with WF.

Shweta D

Hi... Thanks a lot for this article.. it helped me to get a single wsdl for IIS hosted wcf. :-)

Anatoliy

Hi. Please, help me with my problem: when I tried to use FlatWsdl as behavior for my service, I had two WSDL documents again. There is two endpoints in the service, but situation (2 wsdl's) wasn'n change when I removed one of them.

And addition: in main wsdl there is
wsdl:import namespace="http://ServiceSample" location="http://localhost:8000/service?wsdl=wsdl0"
instead
xsd:import schemaLocation="http://localhost:7777/WSCF?xsd=xsd0" namespace="http://www.thinktecture.com/services/restaurant/2006/12"

("wsdl:import ... ?wsdl=wsdl0" instead "xsd:import ... ?xsd=xsd0")
Maybe, it is a reason? But I don't understand what is a solution in this case too.

I am writing a Delphi6-client for MS's sample "Hosting a WCF Service in WPF" http://msdn.microsoft.com/en-us/library/ms756474.aspx for check compability our project with olg clients.

Thanks.

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been saved. Comments are moderated and will not appear until approved by the author. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

Comments are moderated, and will not appear until the author has approved them.

Your Information

(Name is required. Email address will not be displayed with the comment.)