Recently, I have encountered a web-services integration scenario where one of our large solution providers are using a java-based system and the cURL command-line on Linux machines to access our Windows Communications Foundation (WCF) web-service. Their system is a legacy system that somewhat pre-dates even the SOAP 1.1 standards.
Basically, here are the requirements for their web-service client application:
- The web-service that we provide should somewhat behave like an ASP.NET ASMX service.
- The body of the SOAP envelop will contain multiRef
elements. - There will be no SoapAction in the request header.
When trying to interop WCF with other non-.NET platform clients, the recommended binding is to use the basicHttpBinding. That should provide the basic interop capabilities for most legacy web-services client - including ASMX clients.
That of course did not suffice in our scenario because there are multiRef elements in our packets. On closer inspection, we could see that the provider is using encoded format instead of the default literal format in WCF. To support the encoded format, we need to apply the XmlSerializerFormat attribute to the ServiceContract.
[ServiceContract]
[XmlSerializerFormat(Style = OperationFormatStyle.Rpc, Use = OperationFormatUse.Encoded)]
public interface IMyService
We will need to specify RPC for the Style parameter in order to use the Encoded format. By default, WCF uses Document style with Literal format.[XmlSerializerFormat(Style = OperationFormatStyle.Rpc, Use = OperationFormatUse.Encoded)]
public interface IMyService
At this point, if the service was tested with tools such as SOAP UI, it will work perfectly. However, when testing from cURL, it will fail with the following exception:
"The message with Action '' cannot be processed at the receiver, due to a ContractFilter mismatch at the EndpointDispatcher. This may be because of either a contract mismatch (mismatched Actions between sender and receiver) or a binding/security mismatch between the sender and the receiver. Check that sender and receiver have the same contract and the same binding (including security requirements, e.g. Message, Transport, None)."
If you search around, you will find that most of the answers will tell you about out-dated service proxies, wrong security settings or mismatch bindings on both client and server ends. All of those explanation cannot be applied in this scenario because our service client is not a .NET client - so they don't "Add Service Reference" and don't have bindings.
The message actually meant that WCF (being a good technology that follows standards) was unable to route to the correct operation (a.k.a. call the method) when the Action is set to empty. Beginning from SOAP 1.1, it is stated that web-services should use the SoapAction header to determine which operation to invoke in a web-service. However, pre SOAP 1.1 clients can still use empty Actions.
[Note: You should be puzzled as to why it can work on SOAP UI but not cURL. The answer to the mystery is SOAP UI intelligently injects the SoapAction header for us.]
To support empty Actions, we can specify it in the OperationContracts:
[OperationContract(Action="")]
ResponseMessage DoOperationA(RequestMessage message);
[OperationContract(Action="")]
ResponseMessage DoOperationB(RequestMessage message);
After making the changes, the code will compile successfully but will throw another exception when we try to access the WSDL of the service.
"The operations DoOperationA and DoOperationB have the same action (). Every operation must have a unique action value."
To solve this problem, I managed to rip some code from the following MSDN sample - Route By Body. The sample is actually part of the WF_WCF_Samples package. We will need to create 2 classes - an Attribute and a DispatchOperationSelector.
The code for the Attribute is as follows (copy-pasted and modified a little from the sample):
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
The final part to enable our service to support empty Actions is to decorate the attribute on our ServiceContract.
There you go. Hope it helps!
"The operations DoOperationA and DoOperationB have the same action (). Every operation must have a unique action value."
To solve this problem, I managed to rip some code from the following MSDN sample - Route By Body. The sample is actually part of the WF_WCF_Samples package. We will need to create 2 classes - an Attribute and a DispatchOperationSelector.
The code for the Attribute is as follows (copy-pasted and modified a little from the sample):
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
public class EmptyActionBehaviorAttribute : Attribute, IContractBehaviorI had renamed my attribute to EmptyActionBehaviorAttribute. The DispatchOperationSelector code provided in the sample somehow did not work for me. Therefore, I had to make some modifications. The code is as follows:
{
#region IContractBehavior Members
public void AddBindingParameters(ContractDescription contractDescription,
ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
return;
}
public void ApplyClientBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
return;
}
public void ApplyDispatchBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
{
var dispatchDictionary = new Dictionary<XmlQualifiedName, string>();
foreach (OperationDescription operationDescription in contractDescription.Operations)
{
XmlQualifiedName qname =
new XmlQualifiedName(operationDescription.Messages[0].Body.WrapperName,
operationDescription.Messages[0].Body.WrapperNamespace);
dispatchDictionary.Add(qname, operationDescription.Name);
}
dispatchRuntime.OperationSelector =
new EmptyActionOperationSelector(dispatchDictionary);
}
public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
{
}
#endregion
}
class EmptyActionOperationSelector: IDispatchOperationSelector
{
Dictionary<XmlQualifiedName, string> dispatchDictionary;
public EmptyActionOperationSelector(Dictionary<XmlQualifiedName,
string> dispatchDictionary)
{
this.dispatchDictionary = dispatchDictionary;
}
public string SelectOperation(ref System.ServiceModel.Channels.Message message)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(message.ToString());
XmlNamespaceManager nsManager = new XmlNamespaceManager(xmlDoc.NameTable);
nsManager.AddNamespace("soapenv", "http://schemas.xmlsoap.org/soap/envelope/");
XmlNode node =
xmlDoc.SelectSingleNode("/soapenv:Envelope/soapenv:Body", nsManager).FirstChild;
XmlQualifiedName lookupQName = new XmlQualifiedName(node.LocalName, node.NamespaceURI);
if (dispatchDictionary.ContainsKey(lookupQName))
{
return dispatchDictionary[lookupQName];
}
else
{
return node.LocalName;
}
}
}
The final part to enable our service to support empty Actions is to decorate the attribute on our ServiceContract.
[ServiceContract]
[XmlSerializerFormat(Style = OperationFormatStyle.Rpc,
Use = OperationFormatUse.Encoded)]
[EmptyActionBehavior]
public interface IMyService
Once that is done, the service should be able to be called from cURL and the java client.There you go. Hope it helps!
No comments:
Post a Comment