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!

About these ads

About varunpuranik
I am a Developer at Microsoft. I work in the App Server Group on WCF RIA Services.

16 Responses to OperationTag–Its Demise and Workarounds

  1. Marco says:

    Hi varun and thanks for the post.
    So, if I’m not wrong, ther’s no hope to have an out of the box implementation of OperationTag, right ?
    I’ve encountered the same problem that you describe in this post and tried to put this info in a cookie and in a httpHeaders. Unfortunately the latter is almost unusable, I’ve tried other browser (chrome and opera) and it seems to work only in IE. Anyway I’m quite happy using the workaround with cookies combined with a message inspector both client and server side. In the client message inspector I put a tag that I can read server side and vice-versa thus having Operation level tracking, don’t you agree ?
    To do so, I’ve tweaked the DomainContext Generation and hooked the ChannelFactory inside the code of the RIA proxy, (of course, just to avoid to do it manually, I actually have 6 different domain services, but plans are to double this number).
    I have to say that it seems to work pretty well, (solution is still under development and ther’s no production release) but I’d like to know your impressions about this solution

    • varunpuranik says:

      Hey Marco,
      Well, I would not say that there will never be a out of the box solution, just that it won’t be in the SP2 release. We will definitely try to add support for it out of the box in one of the future releases.
      As for your solution, yes, it does work and you get Operation level tracking by sending cookies set from the client. However, I am not sure how correct it is semantically, I am not sure. But if that is not a concern for you, then it certainly is an option.

    • highdownts says:

      Can you possibly share a code example for your proposed solution?

      Thanks…

  2. highdownts says:

    Marco,

    I was interested in your workaround approach to see if it can help me simplify my implementation on a large scale e-commerce project. I had been hoping to use the dropped feature to accomplish a RIA services related function. I have many databases with the same schema (50-100) and roughly 7 unique types of databases (350 – 700 total). I want to use one RIA service for each of the unique types of databases.

    What I have had to do is to write a connection string identifier (DBType + unique identifier) to a database on the server and then invoke the RIA service. I override the CreateObjectContext method in the RIA service class. In this method, I read the connection string identifier from the server, select the right connection string, and then complete the RIA service initialization. This unfortunately requires three database operations and two round trips from the client. I am looking for an approach to do this in one shot to the server with only one database operation. What I really need is very simply parameter passing with the RIA constructor, but that isn’t possible.

    Because the planned system will eventually handle hundreds of millions (possibly a lot more) of RIA service operations/year, a simple factor of two or three really can add up for certain types of operations. This isn’t as important for some of the lower use rate RIA services, but if I find a good solution, I’ll implement it across the board.

    Thanks…

  3. Marco says:

    Ok, of course no problem about the sharing of code, I’ll write a post as soon as possible.Basically it permits to write something on the cookie on every request, thus using the cookie like an HttpHeader and giving the possibility to share request-related data between client and server.
    Relating it to your problem, (if I’ve understood it correctly) I’d probably introduce a repository object that wraps a list of ObjectContext and basing on the info it gets from the cookie it returns the correct ObjectContext… but that’s just an idea! I’ll notify you here when I’ll write the post

    Bye

  4. highdownts says:

    Thanks for your assistance.

    I assume that you are talking about creating a domain service factory to generate the correct ObjectContext based on the cookie content. Is that correct? I have been reading up on creating a domain service factory, but don’t have a working model yet. If you know if any good links/examples on the net that are relavant, please let me know.

    Thanks again…

  5. Well, no, I was not referring to DomainServiceFactory, anyway perhaps this is a quicker way of doing it. Using a customized DomainServiceFactory is pretty simple,
    put this line of code in your global.asax / Application_Startup (or equivalent)

    System.ServiceModel.DomainService.Factory = new MyDomainServiceFactory();

    and this is the needed (and absolutely basic) class

    public class MyDomainServiceFactory : IDomainServiceFactory
    {
    #region IDomainServiceFactory Members

    public DomainService CreateDomainService(Type domainServiceType, DomainServiceContext context)
    {
    if (!typeof (DomainService).IsAssignableFrom(domainServiceType))
    {
    throw new ArgumentException(String.Format(“Cannot create an instance of {0} since it does not inherit from DomainService”, domainServiceType), “domainServiceType”);
    }
    //Put the logic of reading the cookie in the line below
    string requestedCnnString = “”;
    //Great point to get your DomainService by an IoC container
    var dmnService = (DomainService) Activator.CreateInstance(domainServiceType);
    //and add logic to change your objectcontext.
    //Remember that when you use a DomainServiceFactory you can also
    //add constructor parameter to your domainService.

    //remember to initialize
    dmnService.Initialize(context);
    return dmnService;
    }

    public void ReleaseDomainService(DomainService domainService)
    {
    domainService.Dispose();
    }

    #endregion
    }

    please note that the code above isn’t compiled so it could contains severe errors.

    Hope this helps

  6. highdownts says:

    It took me a while to get everything right per your suggestions, but I now have a working prototype and have started modifying my main project.

    Thanks again for your assistance…

  7. Pingback: RIA Services

  8. Hi guys,

    Can you share more details why putting the OperationTag in the Message Headers was not an option? You talk about the message body, but nothing has been said about the headers?

    -sdobrev

  9. Wells.Cui says:

    Hi guys,
    When I try the second workaround ” Implementing the OperationTag via HTTP Headers on top of RIA Services”,I encounter error:
    “Operation ‘Invoke’ of contract ‘ICommunicationSrvContract’ specifies multiple request body parameters to be serialized without any wrapper elements. At most one body parameter can be serialized without wrapper elements. Either remove the extra body parameters or set the BodyStyle property on the WebGetAttribute/WebInvokeAttribute to Wrapped.”

    I do set the BodyStyle property on the WebGetAttribute/WebInvokeAttribute to Wrapped on the Service side of the Function “Invoke”. It can’t work.

    • Wells.Cui says:

      when I set the BodyStyle property on the WebGetAttribute/WebInvokeAttribute to Wrapped on the Client side of the Function “Invoke”, function on the service side could not receive the value of parameters.

  10. Dinesh says:

    Hi,
    I tried the second workaround “OperationTag via HTTP Headers ” in my WCF RIA Service. I can get operationtag in server side but when I retrieve set of entities using LoadOperation in Clint side. it is throws error as below

    InnerException Message:

    “Unable to deserialize XML body with root name ‘GetEmployeeResponse’ and root namespace ‘http://tempuri.org/’ (for operation ‘GetEmployee’ and contract (‘IDomainService1Contract’, ‘http://tempuri.org/’)) using DataContractSerializer. Ensure that the type corresponding to the XML is added to the known types collection of the service.”

    Thing that I Missed form your example code is

    1) I commanded [ThreadStatic] in CustomMessageInspector class. Because it is throws the error”Attribute ‘ThreadStatic’ is not valid on this declaration type. It is only valid on ‘field’ declarations”

    Thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: