WCF "Raw" programming model (Web) - receiving arbitrary data

The previous post mentioned how to return arbitrary data from WCF services. To receive data, however, there is one extra step, which I'll try to explain here.

Like returning arbitrary data, the key for accepting arbitrary (in any format) data is for a method to have a parameter with type System.IO.Stream. This parameter needs to be the single parameter which is passed in the body of the request. By that we mean that the operation can have other parameters beside the Stream one, as long as they're used in the address (UriTemplate) for the operation. For example, this program below will simulate an UploadFile operation:

public class BlogPostRaw2
{
[ServiceContract]
    public interface ITest
    {
[OperationContract, WebInvoke(UriTemplate = "UploadFile/{fileName}")]
        void UploadFile(string fileName, Stream fileContents);
}
    public class Service : ITest
    {
        public void UploadFile(string fileName, Stream fileContents)
{
            byte[] buffer = new byte[10000];
            int bytesRead, totalBytesRead = 0;
            do
            {
bytesRead = fileContents.Read(buffer, 0, buffer.Length);
totalBytesRead += bytesRead;
} while (bytesRead > 0);
            Console.WriteLine("Uploaded file {0} with {1} bytes", fileName, totalBytesRead);
}
}
    public static void Test()
{
        string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
        ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
host.AddServiceEndpoint(typeof(ITest), new WebHttpBinding(), "").Behaviors.Add(new WebHttpBehavior());
host.Open();
        Console.WriteLine("Host opened");

HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(baseAddress + "/UploadFile/Test.txt");
req.Method = "POST";
req.ContentType = "text/plain" ;
        Stream reqStream = req.GetRequestStream();
        byte[] fileToSend = new byte[12345];
        for (int i = 0; i < fileToSend.Length; i++)
{
fileToSend[i] = (byte)('a' + (i % 26));
}
reqStream.Write(fileToSend, 0, fileToSend.Length);
reqStream.Close();
        HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
        Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);
host.Close();
}
}

Notice that the (POST) HTTP request was sent to https://machine_name:8000/Service/UploadFile/<name_of_the_file_to_be_uploaded>; on the body of the request were the file contents.

One important note about the line in bold about Content-Type: when returning arbitrary data, specifying the content type is advisable, but not (necessarily) required. When sending arbitrary data to WCF, it is required. That is because the WebMessageEncoder (the inner piece of WCF, which is created when the endpoint uses the WebHttpBinding), needs it to be able to decode the message.

So this appears to works fine, until I decided to send a XML file to the server (showing only part of the client code):

HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(baseAddress + "/UploadFile/Test.xml");
req.Method = "POST";
req.ContentType = "text/xml";
Stream reqStream = req.GetRequestStream();
string fileContents = "<hello>world</hello>";
byte[] fileToSend = Encoding.UTF8.GetBytes(fileContents);
reqStream.Write(fileToSend, 0, fileToSend.Length);
reqStream.Close();
HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);

When this request is sent to the server, the client gets a "400 Bad Request" response. A look at the server traces (enabled via instructions at https://msdn2.microsoft.com/en-us/library/ms733025.aspx) shows the following error:

System.InvalidOperationException: Incoming message for operation 'UploadFile' (contract 'ITest' with namespace 'https://tempuri.org/') contains an unrecognized http body format value 'Xml'. The expected body format value is 'Raw'. This can be because a WebContentTypeMapper has not been configured on the binding. See the documentation of WebContentTypeMapper for more details.

This is right on target. Basically, as I mentioned before, the WebMessageEncoder is actually composed of three "inner" encoders: XML, JSON and Raw. For content-types which map to the first two, the requests will be processed by then; only if neither the XML or the JSON encoder can process the content-type, the Raw will be used. For the first example it worked fine, since text/plain content cannot be processed by XML or JSON. So we need a way to "force" the encoder to always use Raw. As mentioned in the exception, the WebContentTypeMapper is the solution. The code below can now handle all content-types on request (modified parts in bold). Notice that we need to use a custom binding, since the mapper is only accessible via the WebMessageEncodingBindingElment directly, not in the standard WebHttpBinding.

public class BlogPostRaw2
{
[ServiceContract]
    public interface ITest
    {
[OperationContract, WebInvoke(UriTemplate = "UploadFile/{fileName}")]
        void UploadFile(string fileName, Stream fileContents);
}
    public class Service : ITest
    {
        public void UploadFile(string fileName, Stream fileContents)
{
            byte[] buffer = new byte[10000];
            int bytesRead, totalBytesRead = 0;
            do
            {
bytesRead = fileContents.Read(buffer, 0, buffer.Length);
totalBytesRead += bytesRead;
} while (bytesRead > 0);
            Console.WriteLine("Uploaded file {0} with {1} bytes", fileName, totalBytesRead);
}
}
    publicclassMyMapper : WebContentTypeMapper
{ publicoverrideWebContentFormat GetMessageFormatForContentType(string
contentType)
{ returnWebContentFormat.Raw;

// always
}
} staticBinding

GetBinding()
{ CustomBinding result = newCustomBinding(newWebHttpBinding

()); WebMessageEncodingBindingElement webMEBE = result.Elements.Find<WebMessageEncodingBindingElement
>();
webMEBE.ContentTypeMapper = newMyMapper
(); return
result;
}

    public static void Test()
{
        string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
        ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
host.AddServiceEndpoint(typeof(ITest), GetBinding () , "").Behaviors.Add(new WebHttpBehavior());
host.Open();
        Console.WriteLine("Host opened");

HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(baseAddress + "/UploadFile/Test.xml");
req.Method = "POST";
req.ContentType = "text/xml";
Stream reqStream = req.GetRequestStream();
string fileContents = "<hello>world</hello>";
byte[] fileToSend = Encoding.UTF8.GetBytes(fileContents);
reqStream.Write(fileToSend, 0, fileToSend.Length);
reqStream.Close();
HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
        Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);
host.Close();
}
}