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"
...>
- <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"
...>
- <xsd:schema
elementFormDefault="qualified"
targetNamespace="http://www.thinktecture.com/services/restaurant/2006/12">
- <xsd:element
name="RateRestaurant">
<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:element
name="ListRestaurants">
<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: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: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:element
name="zip"
nillable="true"
type="xsd:string">
<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:element
name="restaurants"
nillable="true"
type="q5:RestaurantsList"
xmlns:q5="urn:thinktecture-com:demos:restaurantservice:data:v1">
<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: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:element
name="restaurantID"
type="xsd:int"
/>
- <xsd:element
name="name"
nillable="true"
type="xsd:string">
<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">
<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">
<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">
<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">
<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">
<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">
<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.
|