OperationTag–Its Demise and Workarounds

In the WCF RIA Services V1.0 SP2 Preview (April 2011) release, we introduced a new feature called OperationTag. It allowed you to tag server requests with a Client/Session/Operation Id known as an OperationTag, for client/session/operation tracing/tracking respectively. We have decided to cut this feature from the next release of the V1.0 SP2. I intend to explain the reasons for our decision and other alternatives that you could use instead, in this post.

Original Design and Implementation

As I mentioned before, the OperationTag was intended to be used to tag server requests with an ID for per client/session/operation tracking.  It is strictly metadata and not a part of the Data sent over to the server.

So the options we had to send this additional info were –

  1. As part of the message body

    This would work, technically. However, there were 2 problems with this approach –
    a. The OperationTag is not really part of the “Data” being sent to the server, it is really “metadata”. The data being sent in the body is expected to be consumed on the client. The OperationTag doesn’t quite fit into that category.
    b. And more importantly, currently, the data is sent directly in the body of the message. To be able to send additional metadata in the body, we would have to introduce some sort of enclosing tags to differentiate it form the data. That would be a breaking change.

  2. As a Query parameter on the URL

    Again, this could have worked, technically. But it has problems similar to the one above, viz-
    a. It is semantically incorrect – a query parameter is not the right place to put additional (client tracking) metadata.
    b. And more importantly, this would only work for Queries. For Invoke and Submit Operations, it would not work.

  3. Custom HTTP Header

    HTTP allows adding custom headers that are intended to carry any metadata that needs to be sent across requests and responses. This seemed like the best option to implement the OperationTag. It is correct semantically, it would work for all types of requests (query/invoke/submit) and not break any existing scenarios.
    For these reasons, we decided to go with the HTTP Header approach. That was the implementation we released in the WCF RIA Services V1.0 SP2 Preview (April 2011).

(Note: We also considered some other options, but they were all rejected for various reasons. I will not enumerate all of them, for brevity.)
In the HTTP Headers based implementation, the user could specify the OperationTag on the client which would then be sent to the server on subsequent server calls using a custom HTTP Header. The RIA Services server code would access it from the HTTP header and surface it via the OperationTag property on the DomainServiceContext.

(Note: In this post, the term “OperationTag” is used to denote the Client ID, Session ID or the Operation ID used for tracking purposes).

Demise of the OperationTag

    The HTTP Header based implementation of the OperationTag discussed above, though seemingly straightforward and full-proof, failed in a number of scenarios.

1. Cross-Domain Calls

Custom HTTP Headers are not supported for Cross Domain Calls by the browser. This means you cannot use Cross Domain calls with RIA Services if you use this feature – a restriction we certainly would not want to impose on RIA Services customers.

2. Custom HTTP Headers cause Firefox to crash with Default settings

Firefox crashes if you have custom HTTP Headers in Silverlight and the OOPP (Out of Process Plug-ins) flag turned on (it is on by default). It is a bug external to RIA Services and is being tracked in Bugzilla. The problem goes away if you manually turn the OOPP switch off. But we do want to support all browsers in their default configurations for RIA Services. Not doing that would cause frustration to a large number of RIA Services users out there who may be unaware of this problem.

3. HTTP Headers encoding

HTTP Headers allow only ASCII values. Since the OperationTag is set by the user, it is likely that the user would  use something other then ASCII characters, which would cause the HTTP stack to throw an exception. To get rid of that, we could use some sort of encoding for the OperationTag value. But that would mean 2 things –
– The OperationTag would be less readable “On the wire”.
– Since the Server would always use that encoding to decode the incoming OperationTag value, anyone implementing their custom clients (presumably for the JSON/SOAP endpoints), would have to implement similar encoding schemes for the OperationTag.
Both these might be unnecessary complications for someone intending to send plain ASCII tags.

In short, the current implementation would not work in all scenarios cleanly. Also there didn’t seem to be an alternate solution available right now that would fit all our requirements. Because of these reasons, we decided to pull out the OperationTag feature from the RIA Services codebase.

Workarounds

While we could not find a means to provide a framework level solution for this feature, we have identified some workarounds that many of you who wanted to use this feature, may benefit from. These should be relatively easy to implement in your application. But please be aware of their limitations; these workarounds are provided “as is” with no warranties.

1. Cookies

You can use cookies for client tracking on the server. In this approach, on the first call from the client, the server sets a cookie identifying that particular client on the outgoing response. Thereafter, all the calls from that client will contain that cookie, which the server can use to identify the client.

So on the server, you can use methods like these to set / get the cookie –

private static void SetCookie(string clientID)
{
    HttpContext context = HttpContext.Current;
    if (context != null && context.Response != null)
    {
        if (context.Response.Cookies["ClientIDCookie"] == null)
        {
            context.Response.Cookies.Add(new HttpCookie("ClientIDCookie") { Value = clientID });
        }
    }
}

private static string GetCookie()
{
    HttpContext context = HttpContext.Current;
    string cookieValue = string.Empty;
    if (context != null && context.Request != null)
    {
        HttpCookie clientCookie = context.Request.Cookies["ClientIDCookie"];
        if (clientCookie != null)
        {
            cookieValue = clientCookie.Value;
        }
    }
    return cookieValue;
}

(Note that you can have more sophisticated implementations for cookies. I have used a simple one just as a sample).

Then,  the first time a client connects to the server, you set a cookie on the HttpResponse with a generated ClientID using SetCookie(). After that, all the calls from that client will contain the cookie with the ClientID that you can obtain using GetCookie().

Advantages –

  • The implementation is simple and straightforward and should work in most of the cases.
  • No changes are required on the client.

Disadvantages –

  • Since this approach is implemented by the server, you can only implement Client/Session level tracking with this approach. Operation level tracking is not possible.

2. Implementing the OperationTag via HTTP Headers on top of RIA Services

You can have your own implementation of OperationTag based on HTTP Headers, similar to the one released in WCF RIA Services V1.0 SP2 Preview (April 2011).

To implement this, you need to insert the OperationTag in an HTTP Header on the client. You can do it in a number of ways. One of them is to use a IClientMessageInspector. You would hook up an IClientMessageInspector to the to your DomainContext using a WebHttpBehavior. The inspector would add the OperationTag to a HTTP Header on the outgoing message.

You can set the value of the OperationTag from your main page using a Thread Local Storage property on the MessageInspector.

So on the client, the code would look something like this –

namespace BusinessApplicationNamespace
{
    /// <summary>
    /// Custom client WebHttpBehavior to add our CustomMessageInspector to the runtime.
    /// </summary>
    internal class ClientCustomBehavior : WebHttpBehavior
    {
        public ClientCustomBehavior()
            : base()
        {
        }

        public override void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            base.ApplyClientBehavior(endpoint, clientRuntime);
            clientRuntime.MessageInspectors.Add(new CustomMessageInspector());
        }
    }

    /// <summary>
    /// Custom MessageInspector that will add the OperationTag to the outgoing message.
    /// </summary>
    internal class CustomMessageInspector : IClientMessageInspector
    {
        // Thread specific storage to hold the OperationTag value.
        [ThreadStatic]
        public static string OperationTag { get; set; }

        public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
        {
        }

        public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
        {
            if (OperationTag != null && OperationTag.Length > 0)
            {
                this.AddOperationTagToHttpHeader(ref request);
            }
            return null;
        }

        private void AddOperationTagToHttpHeader(ref Message message)
        {
            // HTTP Header size is typically 4k.
            if (OperationTag.Length > 4096)
            {
                throw new InvalidOperationException("Header too big");
            }

            // First check if the outgoing message has a HttpRequestMessageProperty and use that. Else create a new one.
            HttpRequestMessageProperty httpRequestMessageProperty;
            if (message.Properties.ContainsKey(HttpRequestMessageProperty.Name))
            {
                httpRequestMessageProperty = (HttpRequestMessageProperty)message.Properties[HttpRequestMessageProperty.Name];
            }
            else
            {
                httpRequestMessageProperty = new HttpRequestMessageProperty();
                message.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessageProperty);
            }

            // Add the OperationTag to the header with the name "OperationTag".
            httpRequestMessageProperty.Headers["OperationTag"] = OperationTag;
        }
    }
}

In the main code, you can hook up the CustomWebHttpBehavior to the DomainContext by accessing the ChannelFactory on the DomainClient. You also set the OpertionTag value using the public property on the MessageInspector.

public partial class MainPage : UserControl
{
    DomainService1 ds;
    public MainPage()
    {
        InitializeComponent();

        ds = new DomainService1();

        ChannelFactory factory = ((WebDomainClient<BusinessApplicationNamespace.Web.DomainService1.IDomainService1Contract>)ds.DomainClient).ChannelFactory;
        factory.Endpoint.Behaviors.Add(new ClientCustomBehavior());

        CustomMessageInspector.OperationTag = "Client1";
    }
}

On the server, you can check if the incoming message has a HTTP Header containing the OperationTag. If it does, then you access its value to get your client/session/operation ID.

/// <summary>
/// Get the OperationTag from the HTTP Header if there exists one.
/// </summary>
/// <returns>The OperationTag value is there is one, else null.</returns>
private static string GetOperationTag()
{
    string operationTag = null;
    OperationContext operationContext = OperationContext.Current;
    if (operationContext != null)
    {
        HttpRequestMessageProperty httpRequestMessageProperty = (HttpRequestMessageProperty)operationContext.IncomingMessageProperties[HttpRequestMessageProperty.Name];
        if (httpRequestMessageProperty != null)
        {
            operationTag = httpRequestMessageProperty.Headers["OperationTag"];
        }
    }
    return operationTag;
}

Advantages –

  • Unlike the Cookie based approach, you can implement operation level tracking (along with client/session level tracking) with this approach.

Disadvantages  –

  • This approach has all shortcomings noted above, because of which we could not implement it in the WCF RIA Services product itself. However, for those who can live with them (for eg. if you don’t care about cross domain calls or supporting Firefox default config), then this implementation could work very well for you.

3. Tweaking the DomainService

Now if you want to have operation level tracking and also not have to deal with the shortcomings of the HTTP Headers, then you could modify the domain service itself to carry the OperationTag to the client. This needs to be done differently for each type of server call –

a. Query methods – Add an OperationTag parameter. You can then pass the OperationTag as the query parameter and access the parameter on the server.

b. Submit operations – Add a dummy entity type to your solution that contains a dummy property called, say, OperationTag. Then add this entity to the changeset being submitted. Then on the server, you can retrieve the OperationTag by accessing the property on the dummy entity.

c. Invoke Operation – Add an OperationTag parameter, just like the query method.

Advantages –

  • This approach should work in all the scenarios RIA Services currently supports.

Disadvantages –

  • Unlike the other approaches, it does not sit on top of RIA Services – you need to modify your DomainService to implement this.
  • This solution is “not pretty”. Smile

However, as I said before, all these implementations are fairly simple and straightforward to implement. Hopefully, using one of these, it should not be too heavy to implement the client/session/operation tracking on top of your domain service. If you find any approaches better than these, please let me know!

Happy Coding!