Question

Background

I wrote a WCF service a while ago that makes heavy use of custom operation invokers, error handlers, and behaviors - many of which heavily rely on the input message being of a certain type, or the message's base message type (each DataContract inherits from a base class and a number of interfaces). There are also many unit and integration tests set up for the various interfaces and classes involved. In addition, the software has to go through a vigorous sign off process every time it is modified, and rewriting the service layer is not my idea of fun.

It is currently configured to allow JSON and SOAP requests to come in.

Problem

A client wishes to POST to this service, using a application/x-www-form-urlencoded content-type, due to restrictions in their legacy software. Normally, the service would accept a JSON request that looks like this:

{
"username":"jeff",
"password":"mypassword",
"myvalue":12345
}

And the application/x-www-form-urlencoded message body that the client can send looks a bit like this:

username=jeff&password=mypassword&myvalue=12345

Alternatively, the client has informed me that they could format the message as follows (if its useful):

myjson={username:jeff,password:mypassword,myvalue:12345}

Also consider that the service contrract looks like this:

[ServiceContract(Namespace = "https://my.custom.domain.com/")]
public interface IMyContract {
    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, UriTemplate = "process")]
    MyCustomResponse Process(MyCustomRequest req);
}

I would like to keep MyCustomRequest, and avoid replacing it with a Stream (as per the below links).

I have found a number of posts that suggest how to achieve this using a Stream OperationContract parameter, but in my particular instance it will be a lot of work to change the type of the OperationContract's parameter. The below posts go into some detail:

Using x-www-form-urlencoded Content-Type in WCF

Best way to support "application/x-www-form-urlencoded" post data with WCF?

http://www.codeproject.com/Articles/275279/Developing-WCF-Restful-Services-with-GET-and-POST

Though I haven't found anything particularly useful in any of them.

Question

Is there any way that I can intercept the message before it reaches the operation contract, and convert it from a the client's input to my custom classes, then have the rest of the application treat it as per normal?

Custom Message inspector? Operation selector? It's been a while since I got into the guts of WCF, so I'm a little rusty right now. I spent a while looking for the below image, as I remember using it to remind me of the call stack - if it's still relevant!

https://i.stack.imgur.com/pT4o0.gif

Was it helpful?

Solution

So, I solved this using a message inspector. It's not pretty, but it works for my case!

using System;

public class StreamMessageInspector : IDispatchMessageInspector {
    #region Implementation of IDispatchMessageInspector

    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) {
        if (request.IsEmpty) {
            return null;
        }

        const string action = "<FullNameOfOperation>";

        // Only process action requests for now
        var operationName = request.Properties["HttpOperationName"] as string;
        if (operationName != action) {
            return null;
        }

        // Check that the content type of the request is set to a form post, otherwise do no more processing
        var prop = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
        var contentType = prop.Headers["Content-Type"];
        if (contentType != "application/x-www-form-urlencoded") {
            return null;
        }

        ///////////////////////////////////////
        // Build the body from the form values
        string body;

        // Retrieve the base64 encrypted message body
        using (var ms = new MemoryStream()) {
            using (var xw = XmlWriter.Create(ms)) {
                request.WriteBody(xw);
                xw.Flush();
                body = Encoding.UTF8.GetString(ms.ToArray());
            }
        }

        // Trim any characters at the beginning of the string, if they're not a <
        body = TrimExtended(body);

        // Grab base64 binary data from <Binary> XML node
        var doc = XDocument.Parse(body);
        if (doc.Root == null) {
            // Unable to parse body
            return null;
        }

        var node = doc.Root.Elements("Binary").FirstOrDefault();
        if (node == null) {
            // No "Binary" element
            return null;
        }

        // Decrypt the XML element value into a string
        var bodyBytes = Convert.FromBase64String(node.Value);
        var bodyDecoded = Encoding.UTF8.GetString(bodyBytes);

        // Deserialize the form request into the correct data contract
        var qss = new QueryStringSerializer();
        var newContract = qss.Deserialize<MyServiceContract>(bodyDecoded);

        // Form the new message and set it
        var newMessage = Message.CreateMessage(OperationContext.Current.IncomingMessageVersion, action, newContract);
        request = newMessage;
        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState) {
    }

    #endregion

    /// <summary>
    ///     Trims any random characters from the start of the string. I would say this is a BOM, but it doesn't seem to be.
    /// </summary>
    /// <param name="s"></param>
    /// <returns></returns>
    private string TrimExtended(string s) {
        while (true) {
            if (s.StartsWith("<")) {
                // Nothing to do, return the string
                return s;
            }

            // Replace the first character of the string
            s = s.Substring(1);
            if (!s.StartsWith("<")) {
                continue;
            }
            return s;
        }
    }
}

I then created an endpoint behavior and added it via WCF configuration:

public class StreamMessageInspectorEndpointBehavior : BehaviorExtensionElement, IEndpointBehavior {
    public void Validate(ServiceEndpoint endpoint) {

    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) {

    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) {
        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new StreamMessageInspector());
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) {

    }

    #region Overrides of BehaviorExtensionElement

    protected override object CreateBehavior() {
        return this;
    }

    public override Type BehaviorType {
        get { return GetType(); }
    }

    #endregion
}

Here's the excerpt of the config changes:

<extensions>
    <behaviorExtensions>
        <add name="streamInspector" type="My.Namespace.WCF.Extensions.Behaviors.StreamMessageInspectorEndpointBehavior, My.Namespace.WCF, Version=1.0.0.0, Culture=neutral" />
    </behaviorExtensions>
</extensions>
<behaviors>
    <endpointBehaviors>
        <behavior name="MyEndpointBehavior">
            <streamInspector/>
        </behavior>
    </endpointBehaviors>

QueryStringSerializer.Deserialize() deserializes a querystring into a DataContract (based on the DataMember.Name attribute, or the property name if the DataMember attribute does not exist).

OTHER TIPS

Not sure how free you are to update your ServiceContract, but I would try to extend it as follows:

[ServiceContract(Namespace = "https://my.custom.domain.com/")]
public interface IMyContract {
    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, UriTemplate = "process")]
    MyCustomResponse Process(MyCustomRequest req);

    [OperationContract]
    [WebInvoke(Method = "POST", UriTemplate = "processForm")]
    MyCustomResponse ProcessForm(MyCustomRequest req);
}

and then would give this client new URL to post to.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top