This sample was provided as part of a blog series on WCF extensibility. The entry for this sample can be found at http://blogs.msdn.com/b/carlosfigueira/archive/2011/04/19/wcf-extensibility-message-inspectors.aspx.

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):

C#
Edit|Remove
[DataContract] 
public class Contact 
{ 
    [DataMember] 
    public string Id { getset; } 
    [DataMember] 
    public string Name { getset; } 
    [DataMember] 
    public string Email { getset; } 
    [DataMember] 
    public string[] Telephones { getset; } 
} 
  
[ServiceContract] 
public interface IContactManager 
{ 
    [WebInvoke( 
        Method = "POST", 
        UriTemplate = "/Contacts", 
        ResponseFormat = WebMessageFormat.Json)] 
    string AddContact(Contact contact); 
  
    [WebInvoke( 
        Method = "PUT", 
        UriTemplate = "/Contacts/{id}", 
        ResponseFormat = WebMessageFormat.Json)] 
    void UpdateContact(string id, Contact contact); 
  
    [WebInvoke( 
        Method = "DELETE", 
        UriTemplate = "/Contacts/{id}", 
        ResponseFormat = WebMessageFormat.Json)] 
    void DeleteContact(string id); 
     
    [WebGet(UriTemplate = "/Contacts", ResponseFormat = WebMessageFormat.Json)] 
    List<Contact> GetAllContacts(); 
     
    [WebGet(UriTemplate = "/Contacts/{id}", ResponseFormat = WebMessageFormat.Json)] 
    Contact GetContact(string id); 
  
    [WebGet(UriTemplate = "/ContactsAsText")] 
    Stream GetContactsAsText(); 
} 
 
 

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:

C#
Edit|Remove
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) 
{ 
    if (!request.IsEmpty) 
    { 
        Console.ForegroundColor = ConsoleColor.Green; 
        Console.WriteLine("Incoming message:"); 
        Console.WriteLine(request); 
        Console.ResetColor(); 
    } 
  
    return null; 
} 
 
 

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”.

C#
Edit|Remove
public class ContactManagerService : IContactManager 
{ 
    static List<Contact> AllContacts = new List<Contact>(); 
    static int currentId = 0; 
    static object syncRoot = new object(); 
  
    public string AddContact(Contact contact) 
    { 
        int contactId = Interlocked.Increment(ref currentId); 
        contact.Id = contactId.ToString(CultureInfo.InvariantCulture); 
        lock (syncRoot) 
        { 
            AllContacts.Add(contact); 
            WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Created; 
        } 
  
        return contact.Id; 
    } 
  
    public void UpdateContact(string id, Contact contact) 
    { 
        contact.Id = id; 
        lock (syncRoot) 
        { 
            int index = this.FetchContact(id); 
            if (index >= 0) 
            { 
                AllContacts[index] = contact; 
            } 
        } 
    } 
  
    public void DeleteContact(string id) 
    { 
        lock (syncRoot) 
        { 
            int index = this.FetchContact(id); 
            if (index >= 0) 
            { 
                AllContacts.RemoveAt(index); 
            } 
        } 
    } 
  
    public List<Contact> GetAllContacts() 
    { 
        List<Contact> result; 
        lock (syncRoot) 
        { 
            result = AllContacts.ToList(); 
        } 
  
        return result; 
    } 
  
    public Contact GetContact(string id) 
    { 
        Contact result; 
        lock (syncRoot) 
        { 
            int index = this.FetchContact(id); 
            result = index < 0 ? null : AllContacts[index]; 
        } 
  
        return result; 
    } 
  
    public Stream GetContactsAsText() 
    { 
        StringBuilder sb = new StringBuilder(); 
        WebOperationContext.Current.OutgoingResponse.ContentType = "text/plain"; 
        lock (syncRoot) 
        { 
            foreach (var contact in AllContacts) 
            { 
                sb.AppendLine("Contact " + contact.Id + ":"); 
                sb.AppendLine("  Name: " + contact.Name); 
                sb.AppendLine("  Email: " + contact.Email); 
                sb.AppendLine("  Telephones:"); 
                foreach (var phone in contact.Telephones) 
                { 
                    sb.AppendLine("    " + phone); 
                } 
            } 
        } 
  
        WebOperationContext.Current.OutgoingResponse.ContentType = "text/plain; charset=utf-8"; 
        MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(sb.ToString())); 
        return ms; 
    } 
  
    private int FetchContact(string id) 
    { 
        int result = -1; 
        for (int i = 0; i < AllContacts.Count; i++) 
        { 
            if (AllContacts[i].Id == id) 
            { 
                result = i; 
                break; 
            } 
        } 
  
        if (result < 0) 
        { 
            WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound; 
        } 
  
        return result; 
    } 
} 
 
 

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.

C#
Edit|Remove
public class IncomingMessageLogger : IDispatchMessageInspector, IEndpointBehavior 
{ 
    const string MessageLogFolder = @"c:\temp\"; 
    static int messageLogFileIndex = 0; 
  
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) 
    { 
    } 
  
    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) 
    { 
    } 
  
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) 
    { 
        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this); 
    } 
  
    public void Validate(ServiceEndpoint endpoint) 
    { 
    } 
} 
 
 

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.

C#
Edit|Remove
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) 
{ 
    string messageFileName = string.Format("{0}Log{1:000}_Incoming.txt", MessageLogFolder, Interlocked.Increment(ref messageLogFileIndex)); 
    Uri requestUri = request.Headers.To; 
    using (StreamWriter sw = File.CreateText(messageFileName)) 
    { 
        HttpRequestMessageProperty httpReq = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name]; 
  
        sw.WriteLine("{0} {1}", httpReq.Method, requestUri); 
        foreach (var header in httpReq.Headers.AllKeys) 
        { 
            sw.WriteLine("{0}: {1}", header, httpReq.Headers[header]); 
        } 
  
        if (!request.IsEmpty) 
        { 
            sw.WriteLine(); 
            sw.WriteLine(this.MessageToString(ref request)); 
        } 
    } 
  
    return requestUri; 
} 
  
public void BeforeSendReply(ref Message reply, object correlationState) 
{ 
    string messageFileName = string.Format("{0}Log{1:000}_Outgoing.txt", MessageLogFolder, Interlocked.Increment(ref messageLogFileIndex)); 
  
    using (StreamWriter sw = File.CreateText(messageFileName)) 
    { 
        sw.WriteLine("Response to request to {0}:", (Uri)correlationState); 
        HttpResponseMessageProperty httpResp = (HttpResponseMessageProperty)reply.Properties[HttpResponseMessageProperty.Name]; 
        sw.WriteLine("{0} {1}", (int)httpResp.StatusCode, httpResp.StatusCode); 
  
        if (!reply.IsEmpty) 
        { 
            sw.WriteLine(); 
            sw.WriteLine(this.MessageToString(ref reply)); 
        } 
    } 
} 
 
 

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.

C#
Edit|Remove
private WebContentFormat GetMessageContentFormat(Message message) 
{ 
    WebContentFormat format = WebContentFormat.Default; 
    if (message.Properties.ContainsKey(WebBodyFormatMessageProperty.Name)) 
    { 
        WebBodyFormatMessageProperty bodyFormat; 
        bodyFormat = (WebBodyFormatMessageProperty)message.Properties[WebBodyFormatMessageProperty.Name]; 
        format = bodyFormat.Format; 
    } 
  
    return format; 
} 
 
 

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.

C#
Edit|Remove
private string MessageToString(ref Message message) 
{ 
    WebContentFormat messageFormat = this.GetMessageContentFormat(message); 
    MemoryStream ms = new MemoryStream(); 
    XmlDictionaryWriter writer = null; 
    switch (messageFormat) 
    { 
        case WebContentFormat.Default: 
        case WebContentFormat.Xml: 
            writer = XmlDictionaryWriter.CreateTextWriter(ms); 
            break; 
        case WebContentFormat.Json: 
            writer = JsonReaderWriterFactory.CreateJsonWriter(ms); 
            break; 
        case WebContentFormat.Raw: 
            // special case for raw, easier implemented separately 
            return this.ReadRawBody(ref message); 
    } 
  
    message.WriteMessage(writer); 
    writer.Flush(); 
    string messageBody = Encoding.UTF8.GetString(ms.ToArray()); 
  
    // Here would be a good place to change the message body, if so desired. 
  
    // now that the message was read, it needs to be recreated. 
    ms.Position = 0; 
  
    // if the message body was modified, needs to reencode it, as show below 
    // ms = new MemoryStream(Encoding.UTF8.GetBytes(messageBody)); 
  
    XmlDictionaryReader reader; 
    if (messageFormat == WebContentFormat.Json) 
    { 
        reader = JsonReaderWriterFactory.CreateJsonReader(ms, XmlDictionaryReaderQuotas.Max); 
    } 
    else 
    { 
        reader = XmlDictionaryReader.CreateTextReader(ms, XmlDictionaryReaderQuotas.Max); 
    } 
  
    Message newMessage = Message.CreateMessage(reader, int.MaxValue, message.Version); 
    newMessage.Properties.CopyProperties(message.Properties); 
    message = newMessage; 
  
    return messageBody; 
} 
  
private string ReadRawBody(ref Message message) 
{ 
    XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents(); 
    bodyReader.ReadStartElement("Binary"); 
    byte[] bodyBytes = bodyReader.ReadContentAsBase64(); 
    string messageBody = Encoding.UTF8.GetString(bodyBytes); 
  
    // Now to recreate the message 
    MemoryStream ms = new MemoryStream(); 
    XmlDictionaryWriter writer = XmlDictionaryWriter.CreateBinaryWriter(ms); 
    writer.WriteStartElement("Binary"); 
    writer.WriteBase64(bodyBytes, 0, bodyBytes.Length); 
    writer.WriteEndElement(); 
    writer.Flush(); 
    ms.Position = 0; 
    XmlDictionaryReader reader = XmlDictionaryReader.CreateBinaryReader(ms, XmlDictionaryReaderQuotas.Max); 
    Message newMessage = Message.CreateMessage(reader, int.MaxValue, message.Version); 
    newMessage.Properties.CopyProperties(message.Properties); 
    message = newMessage; 
  
    return messageBody; 
} 
 
 

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.

C#
Edit|Remove
public class Program 
{ 
    public static void Main(string[] args) 
    { 
        string baseAddress = "http://" + Environment.MachineName + ":8000/Service"; 
        ServiceHost host = new ServiceHost(typeof(ContactManagerService), new Uri(baseAddress)); 
        ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(IContactManager), new WebHttpBinding(), ""); 
        endpoint.Behaviors.Add(new WebHttpBehavior()); 
        endpoint.Behaviors.Add(new IncomingMessageLogger()); 
        host.Open(); 
        Console.WriteLine("Host opened"); 
 
        string johnId = SendRequest( 
            "POST", 
            baseAddress + "/Contacts", 
            "application/json", 
            CreateJsonContact(null"John Doe""john@doe.com""206-555-3333")); 
        string janeId = SendRequest( 
            "POST", 
            baseAddress + "/Contacts", 
            "application/json", 
            CreateJsonContact(null"Jane Roe""jane@roe.com""202-555-4444 202-555-8888")); 
 
        Console.WriteLine("All contacts"); 
        SendRequest("GET", baseAddress + "/Contacts"nullnull); 
 
        Console.WriteLine("Updating Jane"); 
        SendRequest( 
            "PUT", 
            baseAddress + "/Contacts/" + janeId, 
            "application/json", 
            CreateJsonContact(janeId, "Jane Roe""jane@roe.org""202-555-4444 202-555-8888")); 
 
        Console.WriteLine("All contacts, text format"); 
        SendRequest("GET", baseAddress + "/ContactsAsText"nullnull); 
 
        Console.WriteLine("Deleting John"); 
        SendRequest("DELETE", baseAddress + "/Contacts/" + johnId, nullnull); 
 
        Console.WriteLine("Is John still here?"); 
        SendRequest("GET", baseAddress + "/Contacts/" + johnId, nullnull); 
 
        Console.WriteLine("It also works with XML payloads:"); 
        string xmlPayload = @"<Contact> 
<Email>johnjr@doe.com</Email> 
<Name>John Doe Jr</Name> 
<Telephones xmlns:a=""http://schemas.microsoft.com/2003/10/Serialization/Arrays""> 
<a:string>333-333-3333</a:string> 
</Telephones> 
</Contact>"; 
        SendRequest( 
            "POST", 
            baseAddress + "/Contacts", 
            "text/xml", 
            xmlPayload); 
 
        Console.WriteLine("All contacts:"); 
        SendRequest("GET", baseAddress + "/Contacts"nullnull); 
    } 
 
    static string SendRequest(string method, string uri, string contentType, string body) 
    { 
        HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(uri); 
        req.Method = method; 
        if (contentType != null) 
        { 
            req.ContentType = contentType; 
        } 
 
        if (body != null) 
        { 
            byte[] bodyBytes = Encoding.UTF8.GetBytes(body); 
            Stream reqStream = req.GetRequestStream(); 
            reqStream.Write(bodyBytes, 0, bodyBytes.Length); 
            reqStream.Close(); 
        } 
 
        HttpWebResponse resp; 
        try 
        { 
            resp = (HttpWebResponse)req.GetResponse(); 
        } 
        catch (WebException e) 
        { 
            resp = (HttpWebResponse)e.Response; 
        } 
 
        Console.ForegroundColor = ConsoleColor.Cyan; 
        Console.WriteLine("Response to request to {0} - {1}", method, uri); 
        Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription); 
        foreach (var headerName in resp.Headers.AllKeys) 
        { 
            Console.WriteLine("{0}: {1}", headerName, resp.Headers[headerName]); 
        } 
 
        Stream respStream = resp.GetResponseStream(); 
        string result = null; 
        if (respStream != null) 
        { 
            result = new StreamReader(respStream).ReadToEnd(); 
            Console.WriteLine(result); 
        } 
 
        Console.WriteLine(); 
        Console.WriteLine("  -*-*-*-*-*-*-*-*"); 
        Console.WriteLine(); 
 
        Console.ResetColor(); 
 
        // Removing the string markers from results (for contact ids) 
        if (result.StartsWith("\"") && result.EndsWith("\"")) 
        { 
            result = result.Substring(1, result.Length - 2); 
        } 
 
        return result; 
    } 
 
    static string CreateJsonContact(string id, string name, string email, string telephones) 
    { 
        string[] phoneNumbers = telephones.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); 
        StringBuilder sb = new StringBuilder(); 
        sb.Append('{'); 
        if (id != null) 
        { 
            sb.AppendFormat("\"Id\":\"{0}\", ", id); 
        } 
 
        sb.AppendFormat("\"Name\":\"{0}\", ", name); 
        sb.AppendFormat("\"Email\":\"{0}\", ", email); 
        sb.Append("\"Telephones\":["); 
        for (int i = 0; i < phoneNumbers.Length; i++) 
        { 
            if (i > 0) sb.Append(", "); 
            sb.AppendFormat("\"{0}\"", phoneNumbers[i]); 
        } 
 
        sb.Append(']'); 
        sb.Append('}'); 
        return sb.ToString(); 
    } 
} 
 
 
 

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 http://wcf.codeplex.com.