One common patterns with web services is the router service. It can hide routing logic from the client or help with load balancing. Wcf 4 ships with libraries and samples to help build such a router very quickly.
Stephen Liedig had notified me recently on a problem which happens when a gSoap client calls a Wcf router. gSoap is a very popular CPP web services stack.
This was the deployment:
The gSoap client was sending this message to the router:
<soap:Envelope xmlns="http://tempuri.org/" xmlns:p="http://myNs/" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Body>
<AddUser>
<user xsi:type="p:User">
<name>yaron</name>
</user>
</AddUser>
</soap:Body>
</soap:Envelope>
If the router would call the service using the http endpoint, everything would work. If it used the tcp endpoint, it could send the message, but the service implementation would never be called and the service would report this exception:
The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:user. The InnerException message was 'Element 'http://tempuri.org/:user' contains data of the ':User' data contract. The deserializer has no knowledge of any type that maps to this contract. Add the type corresponding to 'User' to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding it to the list of known types passed to DataContractSerializer.'. Please see InnerException for more details.
We need to ask two questions: Why this error happens? Why not with Http?
First we must take a look in the message that the router sent to the service. Here is how it appears in the Wcf log:
<soap:Body>
<AddUser>
<user xsi:type="p:User">
<name>yaron</name>
</user>
</AddUser>
</soap:Body>
</soap:Envelope>
Something is missing – the “p” prefix declaration (xmlns:p=http://myNs/) which was present in the message form the client to the router does not appear. This makes this message invalid as it references the undeclared prefix "p" in the "p:User" derived type attribute. But why did the router sent an invalid message when the client sent it a good one? And why not with Http?
Let’s take a look at how Wcf routes messages. We can use the reflector to inspect System.ServiceModel.Routing.SoapProcessingBehavior+SoapProcessingInspector.MarshalMessage():
{
Message message;
MessageVersion version = source.Version;
if ((version == targetVersion) && !RoutingUtilities.IsMessageUsingWSSecurity(understoodHeaders))
{
...
return source;
}
...
else
{
XmlDictionaryReader readerAtBodyContents = source.GetReaderAtBodyContents();
message = Message.CreateMessage(targetVersion, headers.Action, readerAtBodyContents);
}
this.CloneHeaders(message.Headers, headers, to, understoodHeadersSet);
return message;
}
Wcf uses this logic:
- If the input message (from client) and output message (to server) have the same version, and no security is involved, then take the input as is and send it. This explains why the Http case works.
- Otherwise take the input message body and copy it to a new message. Then copy custom user headers.
Since the “p” prefix is not defined under the body but under the root “envelope” element, this prefix is not sent which makes the message invalid. This explains the netTcp bug.
Why Wcf behaves in this way?
Dismissing this behavior as a bug will miss an important discussion. Consider the naïve fix: Parse each attribute under the body and search for the something:something pattern. Since the router has no information about the message semantics it has no way to know if the pattern relates to a prefix or is just a string which looks like this. Moreover doing this would be a huge performance hit, especially with big messages.
What should be the correct behavior?
In order to take both the functional and performance requirements into account, I would recommend the following approach:
- Copy all namespace declarations from the root Envelope and Body elements of the original message into the corresponding elements of the new one (including the default namespace).
- Make sure not to overload the previous prefixes with new definitions in both elements.
Yes, I can think of a few edge cases where this scheme fails. But a more comprehensive solution would cost in performance.
Conclusion
As it stands now, the default gSoap client fails to call the default Wcf router when protocol bridging is used, which is a real interoperability problem.
And Just to clarify, this web service does not define any derived (=inherited = known) types. For some reason the gSoap default message generation logic uses the xsi:type even on base types. This is not forbidden since it uses the correct type name, although there is no real reason to do it. Anyway this makes this case quite common and not limited to services with derived types.
And, yes, this is one of the reasons not everyone likes Xml.
What's next? get this blog rss updates or register for mail updates!
0 comments:
Post a Comment