Jaa


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]

Carlos Figueira
https://blogs.msdn.com/carlosfigueira
Twitter: @carlos_figueira https://twitter.com/carlos_figueira