vrijdag 25 februari 2011

Adding metadata to RIA service calls

How to add metadata to RIA service calls
How often does it occur that you need to send some metadata which each operation.  This could be some authorization info, client parameters or what have you.  Adding a parameter to each operation is tedious and mind numbing work.  Good thing that there is always a lazy solution. ;)
One thing to keep in mind when programming with RIA services is that it generates a REST based service.  I hear people thinking: “what do I care”.  Well, you should care allot.  It means using SOAP headers to transport extra metadata is out of the question.   There is a solution for this though.  We can still use the HTTP header of the GET/PUT/POST requests to provide the service of more data.  In order to do this, we must realize that RIA services is nothing more than WCF with some added abstraction.  So we can use whatever techniques we are used to in WCF in RIA services. 
Getting down to business
Client side
At first we should inject the users data into the httpHeader when making a request to the service.  We can do this by extending the generated DomainContext class.  This class contains a partial method called OnCreated.  To add behavior to our context object, we will need to extend this method.  We should then access our proxy object to the service.  This is a property in our DomainContext class called DomainClient.  And once we have our proxy, we are on familiar terms and we can start extending it. Below is the code:
    public sealed partial class DomainService
    {
        private void OnCreated()
        {
            dynamic webDomainClient = (WebDomainClient<IDomainServiceContract>)DomainClient;
            ContextFlowEndpointBehavior contextFlowEndpointBehavior = new ContextFlowEndpointBehavior();
            webDomainClient.ChannelFactory.Endpoint.Behaviors.Add(contextFlowEndpointBehavior);

        }
    }
To be able to intercept every call to the service we will need to create a message inspector.  In the message inspector we can modify our request message that is going to be sent to the service.  In this case we will add some parameters to the HttpHeader:
public class ContextFlowEndpointBehavior : IEndpointBehavior
{


    public void IEndpointBehavior_Validate(ServiceEndpoint endpoint)
    {
    }
    void IEndpointBehavior.Validate(ServiceEndpoint endpoint)
    {
        IEndpointBehavior_Validate(endpoint);
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }


    public void IEndpointBehavior_ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
    }
    void IEndpointBehavior.ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        IEndpointBehavior_ApplyDispatchBehavior(endpoint, endpointDispatcher);
    }


    public void IEndpointBehavior_AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }
    void IEndpointBehavior.AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        IEndpointBehavior_AddBindingParameters(endpoint, bindingParameters);
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
    {
    }

    public void IEndpointBehavior_ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        clientRuntime.MessageInspectors.Add(new ContextFlowMessageInspector());
    }
    void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        IEndpointBehavior_ApplyClientBehavior(endpoint, clientRuntime);
    }

    public void Validate(ServiceEndpoint endpoint)
    {
    }
}

public class ContextFlowMessageInspector : IClientMessageInspector
{

    public object IClientMessageInspector_BeforeSendRequest(ref Message request, IClientChannel channel)
    {

        string myHeaderName = "foo";
        string myheaderValue = "bar";

        HttpRequestMessageProperty prop = (HttpRequestMessageProperty)request.Properties(HttpRequestMessageProperty.Name);
        prop.Headers(myHeaderName) = myheaderValue;
        return null;
    }
    object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        return IClientMessageInspector_BeforeSendRequest(request, channel);
    }



    public void IClientMessageInspector_AfterReceiveReply(ref Message reply, object correlationState)
    {
    }
    void IClientMessageInspector.AfterReceiveReply(ref Message reply, object correlationState)
    {
        IClientMessageInspector_AfterReceiveReply(reply, correlationState);
    }
}


Server side
Once we’re done injecting into the HttpHeader of the request client side, we have to be able to read the HttpHeader before it processed by the service.  We can do this by adding another message inspector and extracting the information like this:
public class OperationBehaviorAttribute : Attribute, IOperationBehavior
{


    public void Validate(OperationDescription operationDescription)
    {
    }

    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
    {
        if (dispatchOperation.Parent.MessageInspectors.OfType<ClientCustomHeadersDispatchMessageInspector>.Count == 0)
        {
            ClientCustomHeadersDispatchMessageInspector inspector = new ClientCustomHeadersDispatchMessageInspector();
            dispatchOperation.Parent.MessageInspectors.Add(inspector);
        }

    }


    public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
    {
    }


    public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
    {
    }
}

   public class ClientCustomHeadersDispatchMessageInspector : IDispatchMessageInspector
    {

        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {

            string foo = HttpContext.Current.Request.Headers("foo");
            return null;
        }

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

Then when you have this data, whatever you want to do with it is up to you…  One of the things I like to do is add the parameter to the cache of the httpContext.  This proves to be very usefull when it is always the same data that is being sent to your service.  Eg: I had to write an application that required the user to log in with a smart card.  Legal issues prevented us from saving the data and we didn’t want to bother the user by forcing him to keep his card inserted.  We cached the card’s data and added the id of the data in the http header.
There are a few downsides to this approach though.  One problem is security.  You need to secure the channel in which your messages are being sent when dealing with sensitive data.  Everyone can read a httpHeader with fiddler.
Another problem is that some browsers (mainly firefox and chrome) encapsulate the Silverlight plugin.  This causes some issues when your Silverlight application tries to write inside the httpheader.  However there is a solution for this.  You can specify that the client handles the HTTP, this allows you finer control over the http calls. (http://msdn.microsoft.com/en-us/library/dd920295%28v=vs.95%29.aspx)
Add this in the constructor of your App.xaml:
WebRequest.RegisterPrefix("http://", System.Net.Browser.WebRequestCreator.ClientHttp)

Here are the advantages and disadvantages summed up
Advantages:
  • Transparent
  •  Eliminates redundant code
Disadvantages:
  • Security
  • Chrome / firefox issues
Hope this proves useful.
Till next time ;-)

3 opmerkingen:

  1. Very nice .
    Have to mention , instead of adding message interceptor in the server side , you can use WebOperationContext to get the header , like this :

    WebOperationContext.Current.IncomingRequest.Headers["ClientGuid"]

    BeantwoordenVerwijderen