WCF WebHttpBinding Custom WebMessageFormat

[Note: For ASP.NET Web API version, read here.]

When web services was first introduced more than a decade ago, some people had a different understanding about it. We can't blame them since the early explanations were quite vague and it was not until the SOAP standards came to be and matured, that web services were able to be standardized.

However, it was a little late for the standards to mature as some non-standardized implementations have managed to leak to some very large systems that are powering important applications i.e. Telco applications. Not only that, as the years passed, SOAP is also deemed as complicated and heavy with more people moving to the simpler and lighter REST-based approach which is favored by modern Internet and mobile applications.

As the industry standards continue to evolve from SOAP to REST and to whatever next, the legacy non-standardized web-services are posing challenges (if not problems) for the older systems to adapt to the new technologies. With variants such as dumping XML to web pages, to dumping comma-delimited values, systems integration has become a mesh of complicated customized APIs between organizations and partners.

These problems are very real in my company and industry as my developers need to deal with them almost everyday. I have been a strong advocate of Windows Communications Foundation (WCF) to replace our aged-old 'passing parameter via web-pages' approach for many years here and we have made much progress for our new and migrated systems. But there are still a majority of legacy systems where we just can't move to WCF because we were held-back by our partner's legacy systems and their lag-behind in technology (and industry standards).

The legacy systems were a chain of systems integrated using .aspx pages to pass parameters and response to each other. The systems use simple comma-delimited response which is not XML or JSON, which poses some challenges to us when we first wanted to use WCF. Here's the illustration of the initial design approach if we are to use WCF to support the legacy systems.

Conveniently naming it the Adapter Interface approach, we thought of creating aspx pages to translate the querystrings into contracts which our WCF services can understand. This method works but at a very large performance cost. As illustrated, the ASPX box is a solid box (which opposed to the transparent SOAP box), which indicates an additional physical tier to relay the call. This causes 2 HTTP hops to the back-end which will degrade the performance. This approach had let us to think that might as well just stick with ASPX.

With some research, we have discovered that we can actually disguise our WCF services to enable aspx-style calls by using REST (webHttpBinding). So we started working on a second approach.

This approach has successfully allow us to pass custom-response strings to our legacy service consumers via our WCF while the consumers assumed that we are still running like aspx pages. Not only that this approach is cleaner, it also improves performance by eliminating the extra ASPX physical tier. However, this approach is not very elegant as it requires us to create 2 service contracts for our services.

The reason is REST standards supports only XML (POX) and JSON but our legacy service consumers do not use those. In order for us to expose our non-standard response strings, we will require to have service contracts that expose a Stream object (also known as WCF REST Raw programming model). This dual contract approach somewhat breaks the WCF promise of configurable endpoints that can support multiple bindings. We would like to have a single service contract that can support a mix of standardized and non-standardized service consumers.

WCF only provides XML and JSON WebMessageFormat. But fortunately with WCF's rich extensibility, I was able to come-out with something to make it more elegant by using the IDispatchMessageFormatter to tap into WCF's processing pipeline and produce a message format that I need. There are 4 classes to build for this solution.

1. Create a custom message formatter by inheriting from IDispatchMessageFormatter.

public class TextFormatter : IDispatchMessageFormatter
{
    private OperationDescription _operation;
    private ServiceEndpoint _endpoint;

    public TextFormatter(OperationDescription operation, 
        ServiceEndpoint endpoint)
    {
        this._operation = operation;
        this._endpoint = endpoint;
    }

    void IDispatchMessageFormatter.DeserializeRequest(
        Message message, object[] parameters)
    {
        throw new NotImplementedException("Formatter only supports response.");
    }

    Message IDispatchMessageFormatter.SerializeReply(
        MessageVersion messageVersion, object[] parameters, object result)
    {
        // Create the reply message.
        Message reply = Message.CreateMessage(messageVersion,
            _operation.Messages[1].Action,
            new TextBodyWriter(result));

        // Set the body format to Raw.
        reply.Properties.Add(WebBodyFormatMessageProperty.Name,
            new WebBodyFormatMessageProperty(WebContentFormat.Raw));

        // Add Http Header.
        var httpProperty = new HttpResponseMessageProperty();
        httpProperty.Headers[HttpResponseHeader.ContentType] = "text/plain";
        reply.Properties.Add(HttpResponseMessageProperty.Name, httpProperty);

        return reply;
    }
}

You will need to implement 2 methods, DeserializeRequest and SerializeReply. Since we are dealing with response, we will implement the SerializeReply method.

We create our own Message object and tells WCF that our response will be in Raw format, which means, it will be in binary form. We also set the Http Headers to inform WCF that the response should be in plain text when it is sent to the service consumer.

Notice there is a ResponseBodyWriter class that is being used? We will implement that next.

2. Implement a custom BodyWriter class.

public class TextBodyWriterBodyWriter
{
    private object _content;

    public TextBodyWriter(object content)
        :base(true)
    {
        this._content = content;
    }

    protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
    {
        // Convert data to string.
        byte[] data = Encoding.ASCII.GetBytes(_content.ToString());

        // The root element for binary stream must be named 'Binary'.
        writer.WriteStartElement("Binary");

        // Write out the data.
        writer.WriteBase64(data, 0, data.Length);
        writer.WriteEndElement();

    }
}

This class simply writes the content as string and converts it to an byte array before sending it to the writer. Take note, I only support strings in this example.

3. Next, we need a custom WebHttpBehavior to make use of our formatter.

public class TextResponseBehavior : WebHttpBehavior
{
    protected override IDispatchMessageFormatter GetReplyDispatchFormatter(
        OperationDescription operationDescription, ServiceEndpoint endpoint)
    {
        var webGet = operationDescription.OperationBehaviors.FirstOrDefault(b => b is WebGetAttribute);

        // Only process operations that meets the following condition:
        // 1. Does not have explicit Response Format defined.
        // 2. Does not return void.
        // 3. Does not return a Stream (a.k.a. raw format).
        if (webGet != null && !((WebGetAttribute)webGet).IsResponseFormatSetExplicitly &&
            operationDescription.SyncMethod.ReturnParameter.ParameterType != typeof(void) &&
            operationDescription.SyncMethod.ReturnParameter.ParameterType != typeof(Stream))
        {
            return new TextFormatter(operationDescription, endpoint);
        }
        else
            return base.GetReplyDispatchFormatter(operationDescription, endpoint);
    }
}

I have included some basic checks in the custom behavior where it will work with existing explicitly defined XML and JSON-based REST service.

4. Finally, we need to implement a Behavior Extension for our custom behavior.

public class TextResponseBehaviorExtension : BehaviorExtensionElement
{
    public override Type BehaviorType
    {
        get { return typeof(TextResponseBehavior); }
    }

    protected override object CreateBehavior()
    {
        return new TextResponseBehavior();
    }
}

With all that completed, we can now use our custom behavior and formatter to enable any standard WCF service to response out our customized non-standardized content (i.e. custom string responses, comma-delimited responses etc.) via configuration. To enable them for any of our WCF services, we simply need to do the following 3 steps:

Step 1 - Register our behavior extension in the system.ServiceModel configuration section.

<extensions>
  <behaviorExtensions>
    <add name="TextResponseBehavior"
     type="Sample.TextResponseBehaviorExtension, Sample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

  </behaviorExtensions>
</extensions>

Step 2 - Register an endpoint behavior.

<behaviors>
  <endpointBehaviors>
    <behavior name="RestToAspNet">
      <webHttp />
      <TextResponseBehavior />
    </behavior>

  </endpointBehaviors>
</behaviors>

Remember to include the webHttp setting to indicate that we are using REST.

Step 3 - Specify to use the endpoint behavior in the endpoint which you would like to support the custom response. Do not specify it on endpoints which you need to comply to standards.

<services>
  <service name="CustomREST.Services.ExpenseService" 
           behaviorConfiguration="DefaultServiceBehavior">
    <endpoint name="webHttpExpenseService" 
              address="" 
              binding="webHttpBinding" 
              behaviorConfiguration="RestToAspNet"
              contract="CustomREST.Services.Contracts.IExpenseService" />
  </service>
</services>

So, if I have an Operation Contract that looks like this:

[OperationContract]
[WebGet(UriTemplate="Get?id={id}")]
Expense Get(int id);

The response for my service will be whatever I implemented in the ToString method of my entity i.e.

ExpenseID=111615,CorrelationID=00000000-0000-0000-0000-000000000000,Description=Test,Employee=,AssignedTo=,Remarks=

With this approach, our WCF services are now more maintainable and backward-interoperable with our legacy service consumers. We are now able to expose http, tcp and msmq endpoints all via configuration depending on the service quality required. Modern service consumers can call our standards-based endpoints while legacy systems can continue to function with our services until their end-of-life.

We are now free to progress ahead to support latest industry standards and technology while not being bogged-down by legacy designs of our partners and also able to leverage on the 'One Service Contract to Rule Them All' philosophy - all thanks to WCF's powerful extensibility features.

[Updated: I renamed the topic title because some readers were confused thinking that this is related to the WCF REST Toolkit which is obsolete. Take note that the solution here refers to the out-of-the-box native supported REST capabilities of WCF via webHttpBinding and does not require any 3rd-party toolkits to be installed.]

No comments:

Post a Comment

Popular Post