WCF Soap Services without a Soap Action

I’ve run into an issue twice now when integrating to an externally hosted service with WCF.  This post will hopefully save someone some time on a rather obscure issue.

For WCF services that contain multiple operations, a unique Action should be given to each operation (according to WS-* guidelines).  For example, the following service breaks this rule as both GetData() and GetDataUsingDataContract() have the same action:

[ServiceContract]

public interface IService1

{

    [OperationContract(Action="", ReplyAction="*")]

    string GetName(GetNameRequest value);

 

    [OperationContract(Action = "", ReplyAction = "*")]

    CompositeType GetData(GetDataRequest composite);

}

Some non-Microsoft technologies (e.g., Java, WebSphere) do not enforce this in the same way that WCF does.  In general this does not pose an issue as WCF can build a compatible proxy for communicating with the service.  The issue arises when there is a need to strictly adhere to a definition or when mocking the service.

In order to be able to host the service, we need to use a custom dispatcher.  Simply put, the dispatcher is used to match up the message received by the service to the correct operation.  The normal dispatcher uses the soap Action for this.

This solution is based on the Microsoft DispatchByBody sample.  I adapted it because the service definition I needed to mock was outside of my control (re., a compiled library) so I was not able to easily add custom attributes to solve this.

Custom Dispatcher

By extending the IDispatchOperationSelector, the message can be routed to the appropriate method.  In this example, I use the incoming request’s root node to match against the operation.  This will work in the scenario where an operation is GetData and the request is GetDataRq.  Not the most interesting example…

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.ServiceModel;

using System.ServiceModel.Channels;

using System.ServiceModel.Dispatcher;

using System.Xml;

 

namespace Spike.DispatchByMessage.Dispatcher

{

    class DispatchByBodyElementOperationSelector : IDispatchOperationSelector

    {

        List<string> dispatchDictionary;

        string defaultOperationName;

 

        public DispatchByBodyElementOperationSelector(List<string> dispatchDictionary, string defaultOperationName)

        {

            this.dispatchDictionary = dispatchDictionary;

            this.defaultOperationName = defaultOperationName;

        }

 

        #region IDispatchOperationSelector Members

 

        private Message CreateMessageCopy(Message message, XmlDictionaryReader body)

        {

            Message copy = Message.CreateMessage(message.Version,message.Headers.Action,body);

            copy.Headers.CopyHeaderFrom(message,0);

            copy.Properties.CopyProperties(message.Properties);

            return copy;

        }

 

        public string SelectOperation(ref System.ServiceModel.Channels.Message message)

        {

            XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents();

            XmlQualifiedName lookupQName = new XmlQualifiedName(bodyReader.LocalName, bodyReader.NamespaceURI);

            message = CreateMessageCopy(message,bodyReader);

 

            var operationName = dispatchDictionary.FirstOrDefault(e => lookupQName.Name.ToLower().Contains(e.ToLower()));            

 

            if(operationName!=null)

            {

                return operationName;

            }

            

            return defaultOperationName;            

        }

 

        #endregion

    }

}

And the plumbing…

First we will associate the operation selector with a new contract behavior:

using System;

using System.Collections.Generic;

using System.Text;

using System.ServiceModel.Description;

using System.Xml;

using System.ServiceModel.Dispatcher;

 

namespace Spike.DispatchByMessage.Dispatcher

{

    [AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)]

    sealed class DispatchByBodyElementBehaviorAttribute : Attribute, IContractBehavior

    {

        #region IContractBehavior Members

 

        public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)

        {

            return;

        }

 

        public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)

        {

            return;

        }

 

        public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime)

        {

            List<string> operations = new List<string>();

            foreach( OperationDescription operationDescription in contractDescription.Operations )

            {

                operations.Add(operationDescription.Name);

            }

            

            dispatchRuntime.OperationSelector = new DispatchByBodyElementOperationSelector(operations, dispatchRuntime.UnhandledDispatchOperation.Name);

        }

 

        public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)

        {

        }

 

        #endregion

    }

}

I then defined a service attribute to apply the contract behavior to all endpoints:

using System;

using System.Collections.Generic;

using System.Linq;

using System.ServiceModel.Description;

using System.Text;

using Spike.DispatchByMessage.Dispatcher;

 

namespace Spike.DispatchByMessage

{

    public class DispatchByBodyElementServiceBehaviorAttribute : Attribute, IServiceBehavior

    {

        public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)

        {

            //

        }

 

        public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)

        {

            //

            foreach (var endpoint in serviceDescription.Endpoints)

            {

                endpoint.Contract.Behaviors.Add(new DispatchByBodyElementBehaviorAttribute());

            }

        }

 

        public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)

        {

            //

        }

    }

 

}

Last, I apply the attribute to my service:

[DispatchByBodyElementServiceBehaviorAttribute]

public class Service1 : IService1

This entry was posted in C#, Java, SoapUI, WCF. Bookmark the permalink.

10 Responses to WCF Soap Services without a Soap Action

  1. Martin Havn says:

    Thanks so much for this. It worked on my first try.

  2. SSE says:

    Really Thanks so much. You don’t know, how much your solution had helped me. I can’t express it. It really helpful. Thanks

  3. Keith Murphy says:

    Thanks a lot for that article.

    It helped me solve a real-life problem.

    The only thing I would add, which may be obvious to others, is to make sure to change, or maybe comment out, any existing OperationContractAttribute markup to be less restrictive.

    You may well have this if you generated your service from a WSDL using a tool like svcutil.exe, wsdl.exe, or WSCF.blue.

    If you don’t, you’ll get errors from calls that don’t have SOAPAction, before your custom dispatcher even gets hit.

    For example, if you have:

    [System.ServiceModel.OperationContractAttribute(Action = “http://mysite/BookLookupRequest”, ReplyAction = “http://mysite/BookLookupResponse”)]

    You probably want to change it to:

    [System.ServiceModel.OperationContractAttribute(Action = “*”, ReplyAction = “http://mysite/BookLookupResponse”)]

  4. Keith Murphy says:

    Hmmm, I don’t think my remarks were accurate, after further testing.

    So far, I can’t get your code working for me if I have more than one type of request that I want to target in this scenario.

    It’s not clear to me how to override OperationContractAttribute and have your code take effect without errors.

    I’ll post again if I uncover the answer.

  5. chilberto says:

    Hello Keith,

    Cheers for the reply. Unless you are prevented to for some reason, I suggest keeping it simple and having a single operation on your contract and stand up multiple endpoints.

    If you are not able to do this or if this is not what you are running into, let me know. Of course, if you have another solution to the issue, let me know.

    Cheers, Jeff

  6. Keith Murphy says:

    There is another solution, Jeff, that I finally (re)discovered that seems to be working fine.

    The original issue, after applying your dispatcher, was that I would still get the following error when trying to invoke any of my operations without using the SOAPAction header:

    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).

    I tried wildcarding one of my operations as in my example above, and the operation was located by name just fine, using your custom dispatcher.

    However, after wildcarding another operation, I was getting a big fat System.ServiceModel.ServiceActivationException, telling me that I couldn’t have more than one operation with an Action of ‘*’.

    Kind of makes sense, but how to make the dispatcher work for all of my operations if they all need to be wildcarded? Catch 22.

    Turns out that what I was running into wasn’t that *all* of them needed to be wildcarded – it’s that *one* of them does. The real issue is that you have to have a default operation or WCF squawks if you send a request without a SOAPAction.

    If I wildcard one operation, and leave the others with their original Action attributes, your custom dispatcher works fine for all operations: requests without SOAPAction are routed to their appropriate name-matched operations, not just the wildcarded one.

    Example:

    [System.ServiceModel.OperationContractAttribute(Action = “*”, ReplyAction = “http://mysite/BookLookupResponse”)]
    BookLookupResponse BookLookup(…)

    [System.ServiceModel.OperationContractAttribute(Action = “http://mysite/BookCheckout”, ReplyAction = “http://mysite/BookCheckoutResponse”)]
    BookCheckoutResponse BookCheckout(…)

    It seems kind of odd to me that WCF insists on this even when you are overriding the dispatcher to ignore SOAPAction, but you may know the reason for this.

    Works for me, though.

    • chilberto says:

      Thanks Keith. Much appreciated.

    • Gustavo Martinez says:

      Thanks a lot to all of you guys.

      It’s been quite frustating to get it work but thanks to chilberto and the comments of Keith Murphy I’ve been able to achieve it.

      I really recommend reading this post specially to those who has to put JAVA (axis2/rampart) and .NET Web services (WCF) working together.

    • Srboslav Subotic says:

      After adding a default Action = “*” operation, while keeping the other operations, as per Keith’s advice, it brought smile back on my face!

      Thank you both so much!

  7. BitWiseGuy says:

    Very nice. In my case I needed to build a SOAP service based on a legacy WSDL formally implemented in Java. The SOAPAction was empty in each service. We didn’t want to force clients to make any changes (if we did we would have just used REST of course). And since WCF will not serve SOAP services with duplicated actions (empty actions for me) I needed to create a WCF service that could handle all those existing requests. This works perfect. Well done. Side note.

Leave a Reply

Your email address will not be published. Required fields are marked *