Message Interception, auditing and logging at WCF pipeline.

A message inspector is an extensibility object that can be used in the service model’s client runtime and dispatch runtime programmatically or through configuration and that can inspect and alter messages after they are received or before they are sent.

Any service (dispatcher) message inspector must implement the two IDispatchMessageInspector methods AfterReceiveRequest and BeforeSendReply.

AfterReceiveRequest is invoked by the dispatcher when a message has been received, processed by the channel stack and assigned to a service, but before it is deserialized and dispatched to an operation. If the incoming message was encrypted, the message is already decrypted when it reaches the message inspector. The method gets the requestmessage passed as a reference parameter, which allows the message to be inspected, manipulated or replaced as required. The return value can be any object and is used as a correlation state object that is passed to BeforeSendReply when the service returns a reply to the current message. In this sample, AfterReceiveRequest delegates the inspection (validation) of the message to the private, local method ValidateMessageBody and returns no correlation state object. This method ensures that no invalid messages pass into the service.

I am quoting the above texts from Microsoft MSDN because they are important and you should read before implementing message inspector.

It’s very important that you don’t try to read the original request message body. If you do, you will not be able to read it again during method invocation and you will be rewarded with the famous error- “This message cannot support the operation because it has been read.” If you need to really read the message body, you will need to make a copy of the message and read the copied version. Same is true for Reply message. If you try to read the reply message, the client will no longer get the original reply. Make sure you make a copy of the reply message and preserve the original version before reading it. Also, you must make sure your interception logic does not raise error unnecessarily.

In this article I will show how you can intercept messages at the server side by implementing IDispatchMessageInspector. Implementing IDispatchMessageInspector alone will not do the job and you will have to wire it with servicemodel’s runtime. We are going to collect some data and save them to an object which then can be persisted in a file or database.

Enough warning is said and there is no need to worry because the interception rocks! Here is the quick 1-2-3 of what we are going to need to make the interceptor work-

1) ServerInspector that implements IDispatchMessageInspector.

2) ServiceBehavior to attach the ServerInspector to DispatchRuntime. Then, you can simply add the ServiceBehavior as an attribute to the service implementation class.

3) If you prefer to attach the ServiceBehavior trhough configuration, you will need to implement ServiceBehaviorExtensionElement.

So, let’s jump on to the codes.

ServerInspector.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Dispatcher;
using System.Net;
using System.ServiceModel.Channels;
using System.ServiceModel;
using System.Runtime.Remoting.Messaging;
using System.Xml.Linq;
using System.Reflection;
using System.ServiceModel.Description;

namespace WCF.Behaviors
{
	internal class ServerInspector : IDispatchMessageInspector
	{
		public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
		{
			ClientInfo cinfo = new ClientInfo();

            //Collect any info that passed in the headers from Client, if any.

            cinfo.ServerTimeStamp = DateTime.Now; //Timestamp after the call is received.
            cinfo.Platform = "WCF";
            OperationDescription operationDesc = GetOperationDescription(OperationContext.Current);
            if (operationDesc != null)
            {
                Type contractType = operationDesc.DeclaringContract.ContractType;
                cinfo.Action = operationDesc.Name;
                cinfo.TypeName = contractType.FullName;
                cinfo.AssemblyName = contractType.Assembly.GetName().Name;
            }

            cinfo.ServerName = Dns.GetHostName();
            cinfo.ServerProcessName = System.AppDomain.CurrentDomain.FriendlyName;

			return cinfo;
		}

		public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
		{
			//correlationState is of type ClientInformation and it is set in AfterReceiveRequest event.
			if (correlationState != null)
			{
				ClientInfo cinfo = correlationState as ClientInfo;
				if (cinfo != null)
				{
					cinfo.ServerEndTimeStamp = DateTime.Now;

                    //It's okay to read the RequestMessage since the operation 
                    //has been completed and message is serialized from stream 
                    //(....stream...).
					//RequestMessage on OperationContext is short lived. 
                    //It would be too late to serialize the RequestMessage 
                    //in another thread (exception raised: Message is closed)
					cinfo.Request = OperationContext.Current.RequestContext.RequestMessage.ToString();
					
					//Message can be read only once. 
					//Create a BufferedCopy of the Message and be sure to set 
                    //the original message set to a value wich has not been 
                    //copied nor read.
					MessageBuffer mb = reply.CreateBufferedCopy(int.MaxValue);
					Message responseMsg = mb.CreateMessage();
					reply = mb.CreateMessage();

                    cinfo.Response = responseMsg.ToString();
						
                    if (reply.IsFault == true)
					{
						cinfo.IsError = true;
					}

					//Log cinfo async here;
				}
			}
		}

        private OperationDescription GetOperationDescription(OperationContext operationContext)
		{
			OperationDescription od = null;
			string bindingName = operationContext.EndpointDispatcher.ChannelDispatcher.BindingName;
			string methodName;
			if (bindingName.Contains("WebHttpBinding"))
			{
				//REST request
				methodName = (string)operationContext.IncomingMessageProperties["HttpOperationName"];
			}
			else
			{
				//SOAP request
				string action = operationContext.IncomingMessageHeaders.Action;
				methodName = operationContext.EndpointDispatcher.DispatchRuntime.Operations.FirstOrDefault(o => o.Action == action).Name;
			}

			EndpointAddress epa = operationContext.EndpointDispatcher.EndpointAddress;
			ServiceDescription hostDesc = operationContext.Host.Description;
			ServiceEndpoint ep = hostDesc.Endpoints.Find(epa.Uri);

			if (ep != null)
			{
				od = ep.Contract.Operations.Find(methodName);
			}

			return od;
		}
	}
}

ServiceBehavior.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.ServiceModel;

namespace WCF.Behaviors
{
	public class ServiceBehavior: Attribute, IServiceBehavior
	{
		public ServiceBehavior()
        {   
        }
		
		#region IServiceBehavior Members

		public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
		{
			
		}

		public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
		{
			foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
			{
				ChannelDispatcher cd = cdb as ChannelDispatcher;

				if (cd != null)
				{
                    foreach (EndpointDispatcher ed in cd.Endpoints)
					{
                        ServerInspector serverInspector = ed.DispatchRuntime.MessageInspectors.Find<ServerInspector>();

                        if (serverInspector == null)
						{
							ed.DispatchRuntime.MessageInspectors.Add(new ServerInspector());
						}
					}
				}
			}
		}

		public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
		{
			
		}

		#endregion IServiceBehavior Members
	}
}

ServiceBehaviorExtensionElement.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Configuration;

namespace WCF.Behaviors
{
	public class ServiceBehaviorExtensionElement : BehaviorExtensionElement
	{
		#region BehaviorExtensionElement Optimized

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

		public override Type BehaviorType
		{
			get { return typeof(ServiceBehavior); }
		}

		#endregion BehaviorExtensionElement Optimized

	}
}

ClientInfo.cs:

using System;
using System.Runtime.Serialization;
using System.Text;
using System.Xml.Serialization;
using System.IO;

namespace WCF.Contracts
{
	/// <summary>
    /// Serializable attribute is used to support non-wcf applications.
    /// DataContractSerilizer supports Serializable attribute
	/// </summary>
    [Serializable, XmlRoot(Namespace = "http://www.aspnet4you.com/services")]
	public class ClientInfo
	{
		/// <summary>
		/// Default constructor
		/// </summary>
		public ClientInfo() { }

		

		

		/// <summary>
		/// Name of the machine, which generates a request for a WCF/Remoting service.
		/// </summary>
		public string MachineName
		{
			get;
			set;
		}

		/// <summary>
		/// Time, when the given request was generated.
		/// </summary>
		public DateTime TimeStamp
		{
			get;
			set;
		}

		/// <summary>
		/// The action or the service method for which, the request is generated.
		/// </summary>
		public string Action
		{
			get;
			set;
		}

		/// <summary>
		/// Name of the client application consuming the message.
		/// </summary>
		public string ApplicationName
		{
			get;
			set;
		}

		
		/// <summary>
		/// Machine name where remoting/WCF service is running.
		/// </summary>
		public string ServerName
		{
			get;
			set;
		}

		/// <summary>
		/// Timestamp immediately after request is received at the WCF/Remoting server
		/// </summary>
		public DateTime ServerTimeStamp
		{
			get;
			set;
		}

		/// <summary>
		/// Timestamp at the WCF/Remoting server immediately before sending the response back to client
		/// </summary>
		public DateTime ServerEndTimeStamp
		{
			get;
			set;
		}

		/// <summary>
		/// Server process name at the WCF/Remoting server
		/// </summary>
		public string ServerProcessName
		{
			get;
			set;
		}

		/// <summary>
		/// WCF/Remoting request XML collected at serverside.
		/// </summary>
		public string Request
		{
			get;
			set;
		}

		/// <summary>
		/// WCF/Remoting response XML collected at serverside.
		/// </summary>
		public string Response
		{
			get;
			set;
		}

		/// <summary>
		/// Indicates if there is an error at serverside.
		/// </summary>
		public bool IsError
		{
			get;
			set;
		}

				
		/// <summary>
		/// Assembly name of the of request type.
		/// </summary>
		public string AssemblyName
		{
			get;
			set;
		}

		/// <summary>
		/// Type name of the of request type including namespace.
		/// </summary>
		public string TypeName
		{
			get;
			set;
		}

		
		/// <summary>
		/// Communication Platform; Remoting or Wcf marked at the Server.
		/// </summary>
		public string Platform
		{
			get;
			set;
		}
	}
}

I am not covering the details on how to use the extension element in the configuration and I would leave that you for further reading at- http://msdn.microsoft.com/en-us/library/aa717047.aspx.

Leave a Reply