WCF - Supporting raw requests for individual operations

In the post about the raw mode for the WCF Web programming model, I showed how to use a content-type mapper to tell WCF to treat all incoming requests as “raw” requests, so we could map any content, including those supported by the formatter, to a Stream parameter. This is done at the encoder level, so all operations in the contract basically share the same raw mode. But if you want to mix / match operations with “normal” parameters with operations with a stream parameter, then it becomes a problem, like in the contract below. If we don’t use a content-type mapper, requests with JSON or XML content cannot reach the Upload operation; if we do use one (to map incoming requests to raw), requests of that same content-type cannot be made to the Add operation.

  1. [ServiceContract]
  2. public class Service
  3. {
  4.     [WebInvoke(BodyStyle = WebMessageBodyStyle.WrappedRequest)]
  5.     public int Add(int x, int y)
  6.     {
  7.         return x + y;
  8.     }
  9.  
  10.     [WebInvoke]
  11.     public string Upload(Stream data)
  12.     {
  13.         return new StreamReader(data).ReadToEnd();
  14.     }
  15. }

One possible solution for this is to split the contract in two (and have them as interfaces), have the service implement both interfaces and have two endpoints. This works, but it has the drawback of splitting the URI space between the two contracts, since two endpoints cannot share the same address.

But it’s possible to implement that behavior as well, with a formatter which a trick that will use another encoder to read the input in the format required by the “typed” operations, and we’ll be able to tag the operations for which we can opt-out of the raw format:

  1. [WebInvoke(BodyStyle = WebMessageBodyStyle.WrappedRequest)]
  2. [NonRaw]
  3. public int Add(int x, int y)
  4. {
  5.     return x + y;
  6. }

The NonRawAttribute class is simply an empty attribute class which implements IOperationBehavior, so I’ll skip it here. The interesting part happens at a new formatter which we’ll implement to do the conversion, and is shown below. When deserializing the request, if the operation with which the formatter is associated it is tagged with the [NonRaw] attribute, we’ll write the message back, and then read with the encoder – which does not have any content-type mapper set. That will create a message tagged with the format corresponding to the request’s Content-Type header, which is what we need for the “typed” operations. There is a big drawback of this process, though – there will be a performance hit (both for execution time and memory usage) for those [NonRaw] operations, since there will be an additional encoding / decoding of the message done in the formatter. For small requests this should not be significant, though.

  1. public class DualModeFormatter : IDispatchMessageFormatter
  2. {
  3.     OperationDescription operation;
  4.     IDispatchMessageFormatter originalFormatter;
  5.     MessageEncoder webMessageEncoder;
  6.     BufferManager bufferManager;
  7.  
  8.     public DualModeFormatter(OperationDescription operation, IDispatchMessageFormatter originalFormatter)
  9.     {
  10.         this.operation = operation;
  11.         this.originalFormatter = originalFormatter;
  12.         this.webMessageEncoder = new WebMessageEncodingBindingElement()
  13.             .CreateMessageEncoderFactory()
  14.             .Encoder;
  15.         this.bufferManager = BufferManager.CreateBufferManager(int.MaxValue, int.MaxValue);
  16.     }
  17.  
  18.     public void DeserializeRequest(Message message, object[] parameters)
  19.     {
  20.         if (this.operation.Behaviors.Find<NonRawAttribute>() != null)
  21.         {
  22.             ArraySegment<byte> buffer = this.webMessageEncoder.WriteMessage(message, int.MaxValue, bufferManager);
  23.             string contentType = ((HttpRequestMessageProperty)message.Properties[HttpRequestMessageProperty.Name])
  24.                 .Headers[HttpRequestHeader.ContentType];
  25.             message = this.webMessageEncoder.ReadMessage(buffer, bufferManager, contentType);
  26.             bufferManager.ReturnBuffer(buffer.Array);
  27.         }
  28.  
  29.         this.originalFormatter.DeserializeRequest(message, parameters);
  30.     }
  31.  
  32.     public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result)
  33.     {
  34.         throw new NotSupportedException("This is a request-only formatter");
  35.     }
  36. }

We also need to set this formatter, and the easiest way is to create a class derived from WebHttpBehavior for that.

  1. public class DualModeWebHttpBehavior : WebHttpBehavior
  2. {
  3.     protected override IDispatchMessageFormatter GetRequestDispatchFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
  4.     {
  5.         IDispatchMessageFormatter originalFormatter = base.GetRequestDispatchFormatter(operationDescription, endpoint);
  6.         return new DualModeFormatter(operationDescription, originalFormatter);
  7.     }
  8. }

We can now test this code. Notice that we’re sending typed (JSON) request to the Add operation, and both typed (JSON) and untyped (text) to the stream operation.

  1. static void Main(string[] args)
  2. {
  3.     string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  4.     ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
  5.     WebHttpBinding binding = new WebHttpBinding();
  6.     binding.ContentTypeMapper = new RawContentTypeMapper();
  7.     ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(Service), binding, "");
  8.     DualModeWebHttpBehavior behavior = new DualModeWebHttpBehavior();
  9.     behavior.DefaultOutgoingResponseFormat = WebMessageFormat.Json;
  10.     endpoint.Behaviors.Add(behavior);
  11.     host.Open();
  12.     Console.WriteLine("Host opened");
  13.  
  14.     SendRequest(baseAddress + "/Add", "POST", "application/json", "{\"x\":3,\"y\":5}");
  15.     SendRequest(baseAddress + "/Upload", "POST", "application/json", "{\"x\":3,\"y\":5}");
  16.     SendRequest(baseAddress + "/Upload", "POST", "text/plain", "some random text");
  17.  
  18.     Console.Write("Press ENTER to close the host");
  19.     Console.ReadLine();
  20.     host.Close();
  21. }

And that’s it. The full code for this post can be found in GitHub at https://github.com/carlosfigueira/WCFSamples/tree/master/MessageFormatter/PerOperationContentTypeMapper.