Creating a WCF IDispatchMessageInspector

Sorry I have not had a chance to post in a while.  I had a really bad flu and then there was a snow storm where I live.  Everything thing is cleared up now and I will be going to work tomorrow.

I am still a little new to wcf.  Today I was presented with an interesting task.  I need the ability to read a custom http header on each request to a wcf service and deny the user access depending on the value in the header.  No problem.  Add a reference to System.ServiceModel.Web, turn on asp.net compatibility mode, and use the WebOperationContext to read the header on request and set the status code on the response.

            string myHeader = WebOperationContext.Current.IncomingRequest.Headers["MyHeader"];
            if (!Authenicate(myHeader))
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Forbidden;
                return;
            }

I have to add this code to every service method though.  I want something modular that I can just to any service with no code.  I want to add this ability to any service just by changing some configuration.  I probably should research the Wcf authentication apis to see how I can hook in.  Unfortunately I did not see how yet but I will research more later on this week.

I did notice a interface called IDispatchMessageInspector.  I thought maybe I could use this to hook in to each request and validate the user.  To make this completely configuration based I need to create a service behavior that I can add in the config file.  The library has some good document on how to create a IDispatchMessageInspector and IServiceBehavior class so we can expose it in the config.

My first step was creating a class that implements both IDispatchMessageInspector and IServiceBehavior.  I stubbed out all the interface methods, implemented IServiceBehavior.ApplyDispatchBehavior like in the library example, and added the necessary configuration.  It looked like I would have a small issue.  I wanted to pass in some configuration values to the service behavior.  I added the configuration below and was treated with an error :\

 <system.serviceModel>
  <behaviors>
   <serviceBehaviors>
    <behavior name="SampleProviderService.SampleServiceBehavior">
      <serviceMetadata httpGetEnabled="true" />
     <serviceDebug includeExceptionDetailInFaults="false" />
      <TestInterceptor />
    </behavior>     
   </serviceBehaviors>
  </behaviors>
   <extensions>
     <behaviorExtensions>
       <add
         name="TestInterceptor"
         type="SampleProviderService.TestRequestInterceptor, SampleProviderService"
        />
     </behaviorExtensions>
   </extensions>
  <services>
   <service behaviorConfiguration="SampleProviderService.SampleServiceBehavior"
    name="SampleProviderService.SampleService">
    <endpoint address="" binding="wsHttpBinding" contract="SampleProviderService.ISampleService">
    </endpoint>
   </service>
  </services>
 </system.serviceModel>

The error message said the type needed to inherit from BehaviorExtensionElement.  I must have missed this part in the library documentation.  The BehaviorExtensionElement class is very straight forward.  It is an abstract class that has a Type property and a create method.  The type property returns the type of the class that implements IServiceBehavior and the Create method creates the service behavior.  The BehaviorExtensionElement  class also inherits from ConfigurationElement so I can specify my own configuration.  I created a new class that inherits from BehaviorExtensionElement but I still got an error.  It looks like you must specify the complete fqn with publictokenkey, version, and culture of the assembly when adding behaviorExtensions.  Also make sure you don't have any line returns and a single space after every comma.  After making this change everything was working.

 <system.serviceModel>
  <behaviors>
   <serviceBehaviors>
    <behavior name="SampleProviderService.SampleServiceBehavior">
      <serviceMetadata httpGetEnabled="true" />
     <serviceDebug includeExceptionDetailInFaults="false" />
      <TestInterceptor providerName="testProvider" />
    </behavior>     
   </serviceBehaviors>
  </behaviors>
   <extensions>
     <behaviorExtensions>
       <add
         name="TestInterceptor"
         type="SampleProviderService.RequestInterceptorBehaviorExtension, SampleProviderService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
        />
     </behaviorExtensions>
   </extensions>
  <services>
   <service behaviorConfiguration="SampleProviderService.SampleServiceBehavior"
    name="SampleProviderService.SampleService">
    <endpoint address="" binding="wsHttpBinding" contract="SampleProviderService.ISampleService">
    </endpoint>
   </service>
  </services>
 </system.serviceModel>

Now that we our wcf interceptor we need to find out how to read the http headers.   The IDispatchMessageInterceptor.AfterReceiveRequest has a Message object parameter that has a Headers property.  It looks like these are soap headers though.  I put a break point in the AfterReceiveRequest method and found what I was looking for.  The Message class has a dictionary property called Properties. In the dictionary I saw a key called 'httpRequest' and an object called HttpRequestMessageProperty.  The HttpRequestMessageProperty gave me access to all the http headers amoung other things.

We now need the ability to cancel the request and send the client a response with a error http status code.  Since the AfterReceiveRequest takes the Message property by ref I thought maybe we can replace the message or set it to null.  Setting the message to null worked great and it caused us to jump right to the BeforeSendReply without invoking the service method.  How will I know there was an error though when the BeforeSendReply method is invoked?  Luckily we have the ability to pass some state from AfterReceiveRequest in to this method.  AfterReceiveRequest returns an object and that object is passed into the BeforeSendReply method.  

        public object AfterReceiveRequest(ref Message request,
            IClientChannel channel, InstanceContext instanceContext)
        {
            object correlationState = null;
            HttpRequestMessageProperty requestMessage = request.Properties["httpRequest"] as HttpRequestMessageProperty;
            if (request == null)
            {
                throw new InvalidOperationException("HttpRequestMessageProperty 'httpRequest' property not found !");
            }
            string authHeader = requestMessage.Headers["MyHeader"];
            if (string.IsNullOrEmpty(authHeader)|| !Authenicate(authHeader))
            {
                WcfErrorResponseData error = new WcfErrorResponseData(HttpStatusCode.Forbidden);
                correlationState = error;
                request = null;
            }

            return correlationState;
        }

        public void BeforeSendReply(ref Message reply, object correlationState)
        {
            WcfErrorResponseData error = correlationState as WcfErrorResponseData;
            if (error != null)
            {
                HttpResponseMessageProperty responseProperty = new HttpResponseMessageProperty();
                reply.Properties["httpResponse"] = responseProperty;
                responseProperty.StatusCode = error.StatusCode;

                IList<KeyValuePair<string, string>> headers = error.Headers;
                if (headers != null)
                {
                    for (int i = 0; i < headers.Count; i++)
                    {
                        responseProperty.Headers.Add(headers[i].Key, headers[i].Value);
                    }
                }
            }
        }

That handles everything.  Attached is the sample code.  Later on this week I look at wcf security apis to see if there is a better to hook in through there.  I will also look at what client needs to do to do to consume these services.  I am new to wcf so I am sure if this is the best way to do this.  If anyone has any ideas or examples please send me an email.  Until next time peace.

 Update : Example config had the IServerBehavior type and not the BehaviorExtension type.  Fixed.

Share/Save/Bookmark

TestRequestInterceptor.cs