次の方法で共有


WCF Extensibility – Message Inspectors

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .

The message inspectors are probably the most used extensibility points of the WCF runtime. Anytime you need to log, inspect, modify or completely replace a message, the two inspector interfaces (IClientMessageInspector for the client side; IDispatchMessageInspector for the server side) are likely to be perfect candidates for the solution. There are so many examples of the dispatch inspector (and also many more examples using client inspectors) being used as solution for problems in the forums that it’s actually hard to pick one scenario to show in this post. I’ll then show one full scenario and briefly mention others.

Public implementations in WCF

None. Most of the runtime extensibility points don’t have any public implementations, although there are a few internal ones for the message inspectors.

Interface declarations

  1. public interface IDispatchMessageInspector
  2. {
  3.     object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext);
  4.     void BeforeSendReply(ref Message reply, object correlationState);
  5. }
  6.  
  7. public interface IClientMessageInspector
  8. {
  9.     void AfterReceiveReply(ref Message reply, object correlationState);
  10.     object BeforeSendRequest(ref Message request, IClientChannel channel);
  11. }

The inspector interfaces have two methods: one after the incoming message is received on the wire and before it’s dispatched to the application (AfterReceive), and one after the outgoing message is received from the application and before it’s encoded to be sent on the wire (BeforeSend). Let’s talk about them individually.

At the server side, implementations of IDispatchMessageInspector get a chance to inspect the incoming message right after the request is received via the AfterReceiveRequest method. Besides the message, the method is also passed the channel through which the message arrived (which contains information about any current sessions, local and remote address, and so on), and the instance context associated with the service (with references to the host, and any extensions associated with it). After the user code on the service operation processed the request, and the reply is created into a Message object, BeforeSendReply is called on the inspector code, with the message object and the correlation state, which is whatever AfterReceiveRequest returned – this way the code can correlate the two parts of the message inspection for a single client request.

One thing that deserves mention is that the Message object to both methods is passed by reference. WCF Message objects can only be “consumed” once – and “consumed” can mean read, written or copied. The message body is essentially a read-once stream, so once it’s consumed it cannot be used again. So if, in the inspector code, one were to read the message, the WCF runtime wouldn’t be able to reuse that message in the rest of its pipeline (i.e., to encode it to send as a reply or to parse it into operation parameters). So if the inspector code needs to read the message, it’s the responsibility of the inspector to recreate the message. The code below shows one way of recreating the message object.

  1. public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
  2. {
  3.     Console.WriteLine("Message:");
  4.     Console.WriteLine(request);
  5.     Console.WriteLine();
  6.     MemoryStream ms = new MemoryStream();
  7.     XmlWriter writer = XmlWriter.Create(ms);
  8.     request.WriteMessage(writer); // the message was consumed here
  9.     writer.Flush();
  10.     ms.Position = 0;
  11.     XmlDocument xmlDoc = new XmlDocument();
  12.     xmlDoc.Load(ms);
  13.     this.ChangeMessage(xmlDoc);
  14.  
  15.     //Now recreating the message
  16.     ms = new MemoryStream();
  17.     xmlDoc.Save(ms);
  18.     ms.Position = 0;
  19.     XmlReader reader = XmlReader.Create(ms);
  20.     Message newMessage = Message.CreateMessage(reader, int.MaxValue, request.Version);
  21.     newMessage.Properties.CopyProperties(request.Properties);
  22.     request = newMessage;
  23.     
  24.     return null;

The list of method calls which would cause a message to be consumed are the following: CreateBufferedCopy, GetBody, GetReaderAtBodyContents, WriteBody, WriteBodyContents and WriteMessage. All others are fine: accessing the message headers is ok, since they’re always buffered, so that doesn’t “invalidate” the message. Accessing the message properties is also safe, as they’re just a named dictionary (buffered). Finally, calling Message.ToString in the message is also safe – if the message body is indeed a read-once stream, it will be represented simply as “... stream ...”. If the message is buffered it’s actually possible that the value returned by ToString will display the whole message body.

At the client side, IClientMessageInspector implementations get a chance to inspect the message after it’s been created based on the operation call, and right before it’s encoded. BeforeSendRequest receives the message (again, passed by reference) and the channel through which the message is being sent. When the response from the server arrives and is decoded (by the message encoder) into a Message object, it’s passed to the client inspector in its AfterReceiveReply method. Like in the server version, the method receives the message object (again, by reference) and the correlation state returned by the BeforeSendRequest method. Notice that if the operation contract is defined as one way, the call to AfterReceiveReply is not made at the client inspector, which makes sense, since one way operations do not have a reply message. Weirdly enough, for dispatch message inspectors, BeforeSendReply is actually called for HTTP, but the reply message passed to it is null.

How to add message inspectors

At the server side: the list of dispatch message inspectors is available at the DispatchRuntime object. The object is typically accessed via the endpoint dispatcher in a call to IEndpointBehavior.ApplyDispatchBehavior, or directly passed to an implementation of IContractBehavior.ApplyDispatchBehavior.

  1. public class MyContractBehavior : IContractBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
  4.     {
  5.         dispatchRuntime.MessageInspectors.Add(new MyInspector());
  6.     }
  7. }
  8. public class MyEndpointBehavior : IEndpointBehavior
  9. {
  10.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  11.     {
  12.         endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new MyInspector());
  13.     }
  14. }

At the client side: the list of client message inspectors is available at the ClientRuntime object.

  1. public class MyContractBehavior : IContractBehavior
  2. {
  3.     public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  4.     {
  5.         clientRuntime.MessageInspectors.Add(new MyClientInspector());
  6.     }
  7. }
  8. public class MyEndpointBehavior : IEndpointBehavior
  9. {
  10.     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  11.     {
  12.         clientRuntime.MessageInspectors.Add(new MyClientInspector());
  13.     }
  14. }

 

Real world scenario: Inspecting non-XML messages

As I mentioned before, all WCF messages are XML-based – the message envelope is an XML Infoset represented in memory, with the (optional) header and the body as child XML elements of the envelope. A message can be directly written to a XML writer, and created based on a XML reader. This works quite well in the XML (including SOAP and POX) world. However, with the introduction of the WCF HTTP programming model in .NET Framework 3.5, WCF started accepting out-of-the-box more types of content (most notably JSON and binary content). But since the whole WCF stack is XML-based, sometimes the behavior of the messages can be counter-intuitive.

Take, for example, a simple message logger, which is typically implemented using the message inspectors described in this post. Let’s say that we have a contact manager, which is implemented as a bunch of operations used primarily by web pages (thus using JSON as the primary message format):

  1. [DataContract]
  2. public class Contact
  3. {
  4.     [DataMember]
  5.     public string Id { get; set; }
  6.     [DataMember]
  7.     public string Name { get; set; }
  8.     [DataMember]
  9.     public string Email { get; set; }
  10.     [DataMember]
  11.     public string[] Telephones { get; set; }
  12. }
  13.  
  14. [ServiceContract]
  15. public interface IContactManager
  16. {
  17.     [WebInvoke(
  18.         Method = "POST",
  19.         UriTemplate = "/Contacts",
  20.         ResponseFormat = WebMessageFormat.Json)]
  21.     string AddContact(Contact contact);
  22.  
  23.     [WebInvoke(
  24.         Method = "PUT",
  25.         UriTemplate = "/Contacts/{id}",
  26.         ResponseFormat = WebMessageFormat.Json)]
  27.     void UpdateContact(string id, Contact contact);
  28.  
  29.     [WebInvoke(
  30.         Method = "DELETE",
  31.         UriTemplate = "/Contacts/{id}",
  32.         ResponseFormat = WebMessageFormat.Json)]
  33.     void DeleteContact(string id);
  34.     
  35.     [WebGet(UriTemplate = "/Contacts", ResponseFormat = WebMessageFormat.Json)]
  36.     List<Contact> GetAllContacts();
  37.     
  38.     [WebGet(UriTemplate = "/Contacts/{id}", ResponseFormat = WebMessageFormat.Json)]
  39.     Contact GetContact(string id);
  40.  
  41.     [WebGet(UriTemplate = "/ContactsAsText")]
  42.     Stream GetContactsAsText();
  43. }

Now let’s say we send the following request to the service:

POST /Contacts HTTP/1.1
Content-Type: application/json
Host: my.host.name.com
Content-Length: 90
Expect: 100-continue

{"Name":"Jane Roe", "Email":"jane@roe.com", "Telephones":["202-555-4444", "202-555-8888"]}

Inside the message inspector, we have a simple implementation which prints the message content:

  1. public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
  2. {
  3.     if (!request.IsEmpty)
  4.     {
  5.         Console.ForegroundColor = ConsoleColor.Green;
  6.         Console.WriteLine("Incoming message:");
  7.         Console.WriteLine(request);
  8.         Console.ResetColor();
  9.     }
  10.  
  11.     return null;
  12. }

Easy, right? However, we’re in the Message world, and that means XML, so instead of seeing the nice JSON request which was sent to the server, what ends up being printed is the message below:

<root type="object">
<Name type="string">Jane Roe</Name>
<Email type="string">jane@roe.com</Email>
<Telephones type="array">
<item type="string">202-555-4444</item>
<item type="string">202-555-8888</item>
</Telephones>
</root>

This throws quite a few people off-balance. What is being printed out is actually equivalent to the incoming JSON, by following the mapping between JSON and XML used in WCF. But that doesn’t help for all the scenarios where one needs to log incoming messages, or even change the JSON in the message. The same would happen if the message was a “raw” message, for operations in which the return type or the operation parameter was of type Stream – see more information on the WCF raw programming model for returning or receiving raw data – what would be printed would be the XML mapping of raw data (the base64binary data wrapped around a <Binary> XML element).

This example will show then how to read such content in a way that can be easily manipulated (the example simply logs it to a file, but it can easily be modified to change the message on the fly as well). And before I go further, the usual disclaimer – this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). Also, for simplicity sake it doesn’t have a lot of error handling which a production-level code would. Also, the contact manager is all stored in memory, a “real” one would have a backing database or something more “persistent”.

First, for completeness sake, the service which implements the contract shown before, which is shown below. The implementation is simple (all in memory), with a lock around operations with the “repository”.

  1. public class ContactManagerService : IContactManager
  2. {
  3.     static List<Contact> AllContacts = new List<Contact>();
  4.     static int currentId = 0;
  5.     static object syncRoot = new object();
  6.  
  7.     public string AddContact(Contact contact)
  8.     {
  9.         int contactId = Interlocked.Increment(ref currentId);
  10.         contact.Id = contactId.ToString(CultureInfo.InvariantCulture);
  11.         lock (syncRoot)
  12.         {
  13.             AllContacts.Add(contact);
  14.             WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Created;
  15.         }
  16.  
  17.         return contact.Id;
  18.     }
  19.  
  20.     public void UpdateContact(string id, Contact contact)
  21.     {
  22.         contact.Id = id;
  23.         lock (syncRoot)
  24.         {
  25.             int index = this.FetchContact(id);
  26.             if (index >= 0)
  27.             {
  28.                 AllContacts[index] = contact;
  29.             }
  30.         }
  31.     }
  32.  
  33.     public void DeleteContact(string id)
  34.     {
  35.         lock (syncRoot)
  36.         {
  37.             int index = this.FetchContact(id);
  38.             if (index >= 0)
  39.             {
  40.                 AllContacts.RemoveAt(index);
  41.             }
  42.         }
  43.     }
  44.  
  45.     public List<Contact> GetAllContacts()
  46.     {
  47.         List<Contact> result;
  48.         lock (syncRoot)
  49.         {
  50.             result = AllContacts.ToList();
  51.         }
  52.  
  53.         return result;
  54.     }
  55.  
  56.     public Contact GetContact(string id)
  57.     {
  58.         Contact result;
  59.         lock (syncRoot)
  60.         {
  61.             int index = this.FetchContact(id);
  62.             result = index < 0 ? null : AllContacts[index];
  63.         }
  64.  
  65.         return result;
  66.     }
  67.  
  68.     public Stream GetContactsAsText()
  69.     {
  70.         StringBuilder sb = new StringBuilder();
  71.         WebOperationContext.Current.OutgoingResponse.ContentType = "text/plain";
  72.         lock (syncRoot)
  73.         {
  74.             foreach (var contact in AllContacts)
  75.             {
  76.                 sb.AppendLine("Contact " + contact.Id + ":");
  77.                 sb.AppendLine(" Name: " + contact.Name);
  78.                 sb.AppendLine(" Email: " + contact.Email);
  79.                 sb.AppendLine(" Telephones:");
  80.                 foreach (var phone in contact.Telephones)
  81.                 {
  82.                     sb.AppendLine(" " + phone);
  83.                 }
  84.             }
  85.         }
  86.  
  87.         WebOperationContext.Current.OutgoingResponse.ContentType = "text/plain; charset=utf-8";
  88.         MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(sb.ToString()));
  89.         return ms;
  90.     }
  91.  
  92.     private int FetchContact(string id)
  93.     {
  94.         int result = -1;
  95.         for (int i = 0; i < AllContacts.Count; i++)
  96.         {
  97.             if (AllContacts[i].Id == id)
  98.             {
  99.                 result = i;
  100.                 break;
  101.             }
  102.         }
  103.  
  104.         if (result < 0)
  105.         {
  106.             WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
  107.         }
  108.  
  109.         return result;
  110.     }
  111. }

Now for the inspector itself. I’ll implement it as both an IDispatchMessageInspector and an IEndpointBehavior to make it easier for adding it to the endpoint I’m interested in logging the messages for. It will contain a folder where to log the messages, plus a counter to create the file name where the messages will be logged. The IEndpointBehavior implementation is simple, only using the ApplyDispatchBehavior method to add that instance to the list of message inspectors in the dispatch runtime.

  1. public class IncomingMessageLogger : IDispatchMessageInspector, IEndpointBehavior
  2. {
  3.     const string MessageLogFolder = @"c:\temp\";
  4.     static int messageLogFileIndex = 0;
  5.  
  6.     public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
  7.     {
  8.     }
  9.  
  10.     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  11.     {
  12.     }
  13.  
  14.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  15.     {
  16.         endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
  17.     }
  18.  
  19.     public void Validate(ServiceEndpoint endpoint)
  20.     {
  21.     }
  22. }

Now for the message inspector implementation. For every incoming or outgoing message, we’ll create a new file in the folder defined in the const field for the class.

  1. public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
  2. {
  3.     string messageFileName = string.Format("{0}Log{1:000}_Incoming.txt", MessageLogFolder, Interlocked.Increment(ref messageLogFileIndex));
  4.     Uri requestUri = request.Headers.To;
  5.     using (StreamWriter sw = File.CreateText(messageFileName))
  6.     {
  7.         HttpRequestMessageProperty httpReq = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
  8.  
  9.         sw.WriteLine("{0} {1}", httpReq.Method, requestUri);
  10.         foreach (var header in httpReq.Headers.AllKeys)
  11.         {
  12.             sw.WriteLine("{0}: {1}", header, httpReq.Headers[header]);
  13.         }
  14.  
  15.         if (!request.IsEmpty)
  16.         {
  17.             sw.WriteLine();
  18.             sw.WriteLine(this.MessageToString(ref request));
  19.         }
  20.     }
  21.  
  22.     return requestUri;
  23. }
  24.  
  25. public void BeforeSendReply(ref Message reply, object correlationState)
  26. {
  27.     string messageFileName = string.Format("{0}Log{1:000}_Outgoing.txt", MessageLogFolder, Interlocked.Increment(ref messageLogFileIndex));
  28.  
  29.     using (StreamWriter sw = File.CreateText(messageFileName))
  30.     {
  31.         sw.WriteLine("Response to request to {0}:", (Uri)correlationState);
  32.         HttpResponseMessageProperty httpResp = (HttpResponseMessageProperty)reply.Properties[HttpResponseMessageProperty.Name];
  33.         sw.WriteLine("{0} {1}", (int)httpResp.StatusCode, httpResp.StatusCode);
  34.  
  35.         if (!reply.IsEmpty)
  36.         {
  37.             sw.WriteLine();
  38.             sw.WriteLine(this.MessageToString(ref reply));
  39.         }
  40.     }
  41. }

Now for the interesting part – the implementation of MessageToString. Incoming messages from the encoder used in the WebHttpBinding are tagged with a property of type WebBodyFormatMessageProperty, which defines which of the inner encoders was used to decode the message (the encoder from that binding is actually composed of three encoders: one for XML content, one for JSON, and one for everything else). That property is also used on outgoing messages to tell the web encoder which of the inner encoders should be used to write the message to the wire. So we’ll define a small helper method to retrieve the format from the message object.

  1. private WebContentFormat GetMessageContentFormat(Message message)
  2. {
  3.     WebContentFormat format = WebContentFormat.Default;
  4.     if (message.Properties.ContainsKey(WebBodyFormatMessageProperty.Name))
  5.     {
  6.         WebBodyFormatMessageProperty bodyFormat;
  7.         bodyFormat = (WebBodyFormatMessageProperty)message.Properties[WebBodyFormatMessageProperty.Name];
  8.         format = bodyFormat.Format;
  9.     }
  10.  
  11.     return format;
  12. }

And now for MessageToString (really this time). For XML and JSON messages, the implementation will write the message into a XmlWriter of the appropriate type. The writer created by the class JsonReaderWriterFactory implements the mapping between JSON and XML I mentioned before, so we’ll use it for JSON messages; for XML messages (or for messages which don’t have the body format property) we’ll use the “normal” XML writer from WCF; for raw messages we’ll deal with them specifically in a separate method. After the message is written, it has been consumed, so we need to recreate it to pass it along the channel stack. Using a reader of the same type and creating a new message using the Message.CreateMessage(XmlDictionaryReader, int, MessageVersion) overload and copying the original message properties (which are not serialized when the message is written out).

For the raw messages, since the format is relatively simple (the binary data, written as base64Binary data, wrapped in a single <Binary> element), we can consume it directly – read the message body, skip the wrapping element then read the whole body at once. In this case I’m always converting the binary data to text, in the general case that may not work (if the binary data is an image, for example), but that’s beyond the scope for this post.

  1. private string MessageToString(ref Message message)
  2. {
  3.     WebContentFormat messageFormat = this.GetMessageContentFormat(message);
  4.     MemoryStream ms = new MemoryStream();
  5.     XmlDictionaryWriter writer = null;
  6.     switch (messageFormat)
  7.     {
  8.         case WebContentFormat.Default:
  9.         case WebContentFormat.Xml:
  10.             writer = XmlDictionaryWriter.CreateTextWriter(ms);
  11.             break;
  12.         case WebContentFormat.Json:
  13.             writer = JsonReaderWriterFactory.CreateJsonWriter(ms);
  14.             break;
  15.         case WebContentFormat.Raw:
  16.             // special case for raw, easier implemented separately
  17.             return this.ReadRawBody(ref message);
  18.     }
  19.  
  20.     message.WriteMessage(writer);
  21.     writer.Flush();
  22.     string messageBody = Encoding.UTF8.GetString(ms.ToArray());
  23.  
  24.     // Here would be a good place to change the message body, if so desired.
  25.  
  26.     // now that the message was read, it needs to be recreated.
  27.     ms.Position = 0;
  28.  
  29.     // if the message body was modified, needs to reencode it, as show below
  30.     // ms = new MemoryStream(Encoding.UTF8.GetBytes(messageBody));
  31.  
  32.     XmlDictionaryReader reader;
  33.     if (messageFormat == WebContentFormat.Json)
  34.     {
  35.         reader = JsonReaderWriterFactory.CreateJsonReader(ms, XmlDictionaryReaderQuotas.Max);
  36.     }
  37.     else
  38.     {
  39.         reader = XmlDictionaryReader.CreateTextReader(ms, XmlDictionaryReaderQuotas.Max);
  40.     }
  41.  
  42.     Message newMessage = Message.CreateMessage(reader, int.MaxValue, message.Version);
  43.     newMessage.Properties.CopyProperties(message.Properties);
  44.     message = newMessage;
  45.  
  46.     return messageBody;
  47. }
  48.  
  49. private string ReadRawBody(ref Message message)
  50. {
  51.     XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents();
  52.     bodyReader.ReadStartElement("Binary");
  53.     byte[] bodyBytes = bodyReader.ReadContentAsBase64();
  54.     string messageBody = Encoding.UTF8.GetString(bodyBytes);
  55.  
  56.     // Now to recreate the message
  57.     MemoryStream ms = new MemoryStream();
  58.     XmlDictionaryWriter writer = XmlDictionaryWriter.CreateBinaryWriter(ms);
  59.     writer.WriteStartElement("Binary");
  60.     writer.WriteBase64(bodyBytes, 0, bodyBytes.Length);
  61.     writer.WriteEndElement();
  62.     writer.Flush();
  63.     ms.Position = 0;
  64.     XmlDictionaryReader reader = XmlDictionaryReader.CreateBinaryReader(ms, XmlDictionaryReaderQuotas.Max);
  65.     Message newMessage = Message.CreateMessage(reader, int.MaxValue, message.Version);
  66.     newMessage.Properties.CopyProperties(message.Properties);
  67.     message = newMessage;
  68.  
  69.     return messageBody;
  70. }

That’s it. With this inspector we can log messages of all types for REST services, in their original format. Another way to implement it would be in a custom message encoder (coming up in this series), in which you can have access to the raw bytes coming from the wire, as well as the content-type of the HTTP request.

Now for some test code which sets up the service with that inspector, and sends some messages to it.

  1.     public class Program
  2.     {
  3.         public static void Main(string[] args)
  4.         {
  5.             string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  6.             ServiceHost host = new ServiceHost(typeof(ContactManagerService), new Uri(baseAddress));
  7.             ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(IContactManager), new WebHttpBinding(), "");
  8.             endpoint.Behaviors.Add(new WebHttpBehavior());
  9.             endpoint.Behaviors.Add(new IncomingMessageLogger());
  10.             host.Open();
  11.             Console.WriteLine("Host opened");
  12.  
  13.             string johnId = SendRequest(
  14.                 "POST",
  15.                 baseAddress + "/Contacts",
  16.                 "application/json",
  17.                 CreateJsonContact(null, "John Doe", "john@doe.com", "206-555-3333"));
  18.             string janeId = SendRequest(
  19.                 "POST",
  20.                 baseAddress + "/Contacts",
  21.                 "application/json",
  22.                 CreateJsonContact(null, "Jane Roe", "jane@roe.com", "202-555-4444 202-555-8888"));
  23.  
  24.             Console.WriteLine("All contacts");
  25.             SendRequest("GET", baseAddress + "/Contacts", null, null);
  26.  
  27.             Console.WriteLine("Updating Jane");
  28.             SendRequest(
  29.                 "PUT",
  30.                 baseAddress + "/Contacts/" + janeId,
  31.                 "application/json",
  32.                 CreateJsonContact(janeId, "Jane Roe", "jane@roe.org", "202-555-4444 202-555-8888"));
  33.  
  34.             Console.WriteLine("All contacts, text format");
  35.             SendRequest("GET", baseAddress + "/ContactsAsText", null, null);
  36.  
  37.             Console.WriteLine("Deleting John");
  38.             SendRequest("DELETE", baseAddress + "/Contacts/" + johnId, null, null);
  39.  
  40.             Console.WriteLine("Is John still here?");
  41.             SendRequest("GET", baseAddress + "/Contacts/" + johnId, null, null);
  42.  
  43.             Console.WriteLine("It also works with XML payloads:");
  44.             string xmlPayload = @"<Contact>
  45.   <Email>johnjr@doe.com</Email>
  46.   <Name>John Doe Jr</Name>
  47.   <Telephones xmlns:a=""https://schemas.microsoft.com/2003/10/Serialization/Arrays"">
  48.     <a:string>333-333-3333</a:string>
  49.   </Telephones>
  50. </Contact>";
  51.             SendRequest(
  52.                 "POST",
  53.                 baseAddress + "/Contacts",
  54.                 "text/xml",
  55.                 xmlPayload);
  56.  
  57.             Console.WriteLine("All contacts:");
  58.             SendRequest("GET", baseAddress + "/Contacts", null, null);
  59.         }
  60.  
  61.         static string SendRequest(string method, string uri, string contentType, string body)
  62.         {
  63.             HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(uri);
  64.             req.Method = method;
  65.             if (contentType != null)
  66.             {
  67.                 req.ContentType = contentType;
  68.             }
  69.  
  70.             if (body != null)
  71.             {
  72.                 byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
  73.                 Stream reqStream = req.GetRequestStream();
  74.                 reqStream.Write(bodyBytes, 0, bodyBytes.Length);
  75.                 reqStream.Close();
  76.             }
  77.  
  78.             HttpWebResponse resp;
  79.             try
  80.             {
  81.                 resp = (HttpWebResponse)req.GetResponse();
  82.             }
  83.             catch (WebException e)
  84.             {
  85.                 resp = (HttpWebResponse)e.Response;
  86.             }
  87.  
  88.             Console.ForegroundColor = ConsoleColor.Cyan;
  89.             Console.WriteLine("Response to request to {0} - {1}", method, uri);
  90.             Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);
  91.             foreach (var headerName in resp.Headers.AllKeys)
  92.             {
  93.                 Console.WriteLine("{0}: {1}", headerName, resp.Headers[headerName]);
  94.             }
  95.  
  96.             Stream respStream = resp.GetResponseStream();
  97.             string result = null;
  98.             if (respStream != null)
  99.             {
  100.                 result = new StreamReader(respStream).ReadToEnd();
  101.                 Console.WriteLine(result);
  102.             }
  103.  
  104.             Console.WriteLine();
  105.             Console.WriteLine(" -*-*-*-*-*-*-*-*");
  106.             Console.WriteLine();
  107.  
  108.             Console.ResetColor();
  109.  
  110.             // Removing the string markers from results (for contact ids)
  111.             if (result.StartsWith("\"") && result.EndsWith("\""))
  112.             {
  113.                 result = result.Substring(1, result.Length - 2);
  114.             }
  115.  
  116.             return result;
  117.         }
  118.  
  119.         static string CreateJsonContact(string id, string name, string email, string telephones)
  120.         {
  121.             string[] phoneNumbers = telephones.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
  122.             StringBuilder sb = new StringBuilder();
  123.             sb.Append('{');
  124.             if (id != null)
  125.             {
  126.                 sb.AppendFormat("\"Id\":\"{0}\", ", id);
  127.             }
  128.  
  129.             sb.AppendFormat("\"Name\":\"{0}\", ", name);
  130.             sb.AppendFormat("\"Email\":\"{0}\", ", email);
  131.             sb.Append("\"Telephones\":[");
  132.             for (int i = 0; i < phoneNumbers.Length; i++)
  133.             {
  134.                 if (i > 0) sb.Append(", ");
  135.                 sb.AppendFormat("\"{0}\"", phoneNumbers[i]);
  136.             }
  137.  
  138.             sb.Append(']');
  139.             sb.Append('}');
  140.             return sb.ToString();
  141.         }
  142.     }

One more thing: we had to deal with lots of different formats and translation between a “XML” message into its own format, which is not something very natural. Some good news is that among the new features coming in an upcoming version of WCF is a new HTTP pipeline, which will make it easier to implement a scenario such as this. And you can actually start using them right now, as there is a preview of the feature in the WCF Codeplex site at https://wcf.codeplex.com.

Coming up

The other inspector interface: IParameterInspector.

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    April 19, 2011
    I just wanted to chime in and let you know that these blog posts have been excellent.  I've found them very valuable, explanations and the simple demo's have been great.Thank You

  • Anonymous
    April 28, 2011
    Very nice article.By the way I have some questions, which are:Is IDispatchMessageInspector.AfterReceiveRequest thread safe?How to pass an object from it to the operation/method called? thanks.

  • Anonymous
    April 28, 2011
    @Brian, thanks!@bohebolo, here you go:No, it's not. If the implementation of AfterReceiveRequest takes some time and multiple clients call the service at once it's possible that there will be multiple threads calling AfterReceiveRequest at the same time.You can use the message properties, which is essentially a dictionary (keyed by string) of objects which are attached to a message object. On the operation, you'd retrieve the object using the operation context, as shown below.       [ServiceContract]       public interface ITest       {           [OperationContract]           int Add(int x, int y);       }       public class Service : ITest       {           public int Add(int x, int y)           {               if (OperationContext.Current.IncomingMessageProperties.ContainsKey("MyPropertyName"))               {                   Console.WriteLine("Property value: {0}", OperationContext.Current.IncomingMessageProperties["MyPropertyName"]);               }               return x + y;           }       }       public class MyInspector : IEndpointBehavior, IDispatchMessageInspector       {           ...           public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)           {               endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);           }           public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)           {               request.Properties.Add("MyPropertyName", "the property value");               return null;           }           public void BeforeSendReply(ref Message reply, object correlationState)           {           }       }

  • Anonymous
    May 03, 2011
    The comment has been removed

  • Anonymous
    June 16, 2011
    I'm trying to follow this as closely as possible with my own code, but no messages (requests or replies) seem to have the WebBodyFormatMessageProperty. (Even when the body of the inspector methods are empty.) The inspector gets invoked, but it only has the 3 properties "AllowOutputBatching", "ActivityId" and "httpResponse". Something seems significantly wrong here. Any ideas?

  • Anonymous
    June 20, 2011
    I think I've resolved the problem. If the contract is decorated with a ResponceFormat such as JSON, then the "WebBodyFormatMessageProperty" is present in the reply properties. If it's decorated with WebMessageFormat.Xml, the property is not in the Message object.

  • Anonymous
    June 20, 2011
    I've added some information as 'community content.' Carlos, it would be great if you could chime in on this. I spent about a day and half on this. :(msdn.microsoft.com/.../system.servicemodel.channels.webbodyformatmessageproperty.aspx

  • Anonymous
    June 21, 2011
    Right Ross, you're correct. The default value for the web encoder is Xml (after all it has to choose one encoder to write a message to the wire eventually). If that property is present, then the web encoder will use the format specified on it; otherwise it will choose XML.

  • Anonymous
    June 22, 2011
    Thanks Carlos! I kept looking for the property but couldn't figure out why it was missing. :)

  • Anonymous
    December 26, 2011
    Hi, I downloaded the code and ran the sample. All is great, except that the "request" parameter of "AfterReceiveRequest" is always empty "request.IsEmpty = true" for Get operations. Can you please explain why is that? I want to have an inspector that logs all operations including Get... Thanks in advance

  • Anonymous
    December 26, 2011
    Hi Mohamad, that means that the request does not have a body (which is the case for GET requests). For such requests, the information is actually in the request header (URI and HTTP headers). To access the HTTP headers, you can use the HttpRequestMessageProperty property from the request, and to get the URI you can access the To header. The code below shows an example of that.   public class BlogPost_MessageInspectors_20111226   {       [ServiceContract]       public class Service       {           [WebGet]           public int AddGet(int x, int y) { return x + y; }           [WebInvoke(BodyStyle = WebMessageBodyStyle.WrappedRequest)]           public int AddPost(int x, int y) { return x + y; }       }       class MyBehavior : WebHttpBehavior, IDispatchMessageInspector       {           public override void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)           {               base.ApplyDispatchBehavior(endpoint, endpointDispatcher);               endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);           }           public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)           {               Console.WriteLine(request.Headers.To);               HttpRequestMessageProperty prop = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;               foreach (var header in prop.Headers.AllKeys)               {                   Console.WriteLine("{0}: {1}", header, prop.Headers[header]);               }               return null;           }           public void BeforeSendReply(ref Message reply, object correlationState)           {           }       }       public static void Test()       {           string baseAddress = "http://" + Environment.MachineName + ":8000/Service";           ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));           host.AddServiceEndpoint(typeof(Service), new WebHttpBinding(), "").Behaviors.Add(new MyBehavior());           host.Open();           Console.WriteLine("Host opened");           WebClient c = new WebClient();           Console.WriteLine(c.DownloadString(baseAddress + "/AddGet?x=2&y=5"));           Console.Write("Press ENTER to close the host");           Console.ReadLine();           host.Close();       }   }

  • Anonymous
    December 26, 2011
    Awesome! Many thanks. Your posts are helping my entire organization make use of RESTful WCF...

  • Anonymous
    January 09, 2012
    The comment has been removed

  • Anonymous
    January 09, 2012
    Hi Faheem, do you get the error when running the sample out of the code gallery? I just downloaded it into a new machine, and running it with F5 worked just fine. Or are you changing the sample? Thanks!

  • Anonymous
    January 09, 2012
    Carlos, I copied over the inspector class and added behavior on my endpoint via code. Also switched logging to log4net.The difference that I think could be is that I am adding WebHttpBehavior programmatically and that might have changed the order in which behaviors are applied. If WebHttpBehavior is kicked-in before inspector then we have to copy headers as well. What do you think?

  • Anonymous
    February 29, 2012
    Hi Faheem, it's possible. If you can post your code somewhere (skydrive, dropbox, etc.) I could take a look.

  • Anonymous
    July 09, 2012
    Organized content is the best way to display or post an article, thank you for making it easy to digest your post.<a href="http://www.agpic.com">AGPIC</a>

  • Anonymous
    November 29, 2012
    I'm seeing problems the BeforeSendReply if the web service method being called does NOT return anything (void return rather than a string as in your example).  The reply.IsEmpty is false and when it reaches the following like in the MessageToString method it throws an Unexpected End of FIle error (for the obvious reason that the MemoryStream has a length of zero):reader = XmlDictionaryReader.CreateTextReader(ms, XmlDictionaryReaderQuotas.Max);Is there something on the Message class that I can use to avoid calling MessageToString in this case?

  • Anonymous
    August 08, 2015
    Hi Carlos, Thanks for the extensive and clear article. Is there a way to share custom context (correlation state) between request and reply formatters (generated by WebHttpBehavior.GetRequestDispatchFormatter and WebHttpBehavior.GetReplyDispatchFormatter respectively)? I want to alter the response JSON conversion based on request format (specifically enum value conversion). I did not find a way to pass that information from request formatter to response formatter. Thanks, Barak

  • Anonymous
    August 09, 2015
    Figured it out using OperationContext.Current.RequestContext.RequestMessage.Properties