Partager via


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.

Comments

  • Anonymous
    February 08, 2013
    Hi Carlos,I have a REST based service returning JSON data using VS 2010. I am using WebHttpbinding, security mode of "TransportCredentialOnly" with clientCredentialType as Basic. In the Servicebehavior, I am using usernameAuth with customUserNamePasswordValidatorType set to use a customer username n pwd validator and  userNamePasswordValidationMode="Custom". This is hosted on console app and when i try to navigate to the url, I get a credential pop up asking for a username and password which ties back to the routine I have written in the "customUserNamePasswordValidatorType " above. All this is good.The problem occurs when I host this on my local web server (IIS 7.5) instead of the console app.Using the same config, I get this error below. Note that for hosting on IIS and using REST , I am using System.web.routing to add a serviceroute using the webservicehostfactory (NOT using the .svc approach). Am I missing something here? I tried setting the IIS Authentication for the website to Anonymous which gives the error below. IF I set it to "Basic" I get the pop up for credentials but it does not tie back toe the routine I wrote above. Please let me know.The authentication schemes configured on the host ('Ntlm, Anonymous') do not allow those configured on the binding 'WebHttpBinding' ('Basic').  Please ensure that the SecurityMode is set to Transport or TransportCredentialOnly.  Additionally, this may be resolved by changing the authentication schemes for this application through the IIS management tool, through the ServiceHost.Authentication.AuthenticationSchemes property, in the application configuration file at the <serviceAuthenticationManager> element, by updating the ClientCredentialType property on the binding, or by adjusting the AuthenticationScheme property on the HttpTransportBindingElement.
  • Anonymous
    June 27, 2013
    Carlos, thank you for this useful information. I was able to get it working for my purposes. A minor problem I had to contend with before resolving this WCF problem was disabling IIS's filename extension filter to prevent it from denying uploads of certain files to my file upload operations.I am using your raw-request code to enable selected REST services for my web application. I inserted this code into my WebServiceHostFactory, and blindly assigned the WebHttpBinding to my service; WCF started throwing an unexpected exception when I tried to upload a (bigger than trivially small) file. After several days of researching, debugging and finally, service tracing, I discovered that instead of replacing the binding with the raw formatter, it should be updating the existing binding. This way, the binding configuration can be maintained in the application's web.config file and avoid the errors caused by using a default binding (with its small buffers).I also implemented this by modifying the WebServiceHostFactory that is responsible for instantiating and hosting my service classes. It now recognizes a [RawContentServiceAttribute] on the service class, to insert the DualModeFormatter, and a [RawContentOperationAttribute] on the raw data operation method to actually use the new formatter on only the operations that need it. This limits any performance effects to a few unmarked operations in a single service. To enable this functionality now, I only need to tag my service classes and methods with these attributes.Thanks again for this educational article.