Carlos' blog

Debugging tips and small examples for WCF

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 = "http://" + 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 http://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 http://msdn2.microsoft.com/en-us/library/ms733025.aspx) shows the following error:

System.InvalidOperationException: Incoming message for operation 'UploadFile' (contract 'ITest' with namespace 'http://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);
        }
    }
    public class MyMapper : WebContentTypeMapper
    {
        public override WebContentFormat GetMessageFormatForContentType(string
contentType)
        {
            return WebContentFormat.Raw;
// always
        }
    }
    static Binding
GetBinding()
    {
        CustomBinding result = new CustomBinding(new WebHttpBinding
());
        WebMessageEncodingBindingElement webMEBE = result.Elements.Find<WebMessageEncodingBindingElement
>();
        webMEBE.ContentTypeMapper =
new MyMapper
();
        return
result;
    }
    public static void Test()
    {
        string baseAddress = "http://" + 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();
    }
}

 

Published Thursday, April 17, 2008 11:03 PM by carlosfigueira

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

eggshelly said:

Awesome article Carlos - it helped me almost solve my seeming insurmountable wcf file upload problem.

But of course, I have a problem:

I've implemented your code from above but I'm trying to use the browser as a client.  I used YUI from a simple HTML page to submit a form containing a file upload control.  When I upload any kind of file, the web service receives the request but when it saves the file, the end result is surrounded with meta-data:

-----------------------------1654974419512

Content-Disposition: form-data; name="upFile"; filename="amazon phone number.txt"

Content-Type: text/plain

[file content here - perfectly reproduced (if I remove the surrounding junk)]

-----------------------------1654974419512--

So there must be some difference between how the POST is coming across from YUI and how it's coming across from the HttpWebRequest class, but I can't figure out what that is.  Any ideas?

My web config is set up like:

 <system.serviceModel>

   <behaviors>

     <endpointBehaviors>

       <behavior name="TestBehavior">

         <webHttp/>

       </behavior>

     </endpointBehaviors>

   </behaviors>

   <bindings>

     <webHttpBinding>

       <binding name="fileUploadCapable" maxReceivedMessageSize="500000000" transferMode="Streamed"/>

     </webHttpBinding>

   </bindings>

   <services>

     <service name="Test">

       <endpoint address="ajax" behaviorConfiguration="TestBehavior"

         binding="webHttpBinding" bindingConfiguration="fileUploadCapable"

         contract="ITest" />

     </service>

   </services>

 </system.serviceModel>

And my service has the following interface:

       [OperationContract]

       [WebInvoke(UriTemplate = "UploadFile?upFile={fileName}")]

       void UploadFile(string fileName, Stream fileContents);

and implementation:

       public void UploadFile(string fileName, Stream fileContents)

September 23, 2008 8:45 PM
 

BlogSvc News said:

I've put a new release up on codeplex. This release includes an implementation of Atom…

December 22, 2008 2:49 PM
 

jimblust said:

Carlos, this is a great example, and I have it working in a console app. I've managed to modify the example so that the service can be hosted in IIS, but my tests indicate that IIS always waits to receive the entire request before handing it off to WCF for processing. Do you know if there's any way to make IIS hand off the streaming request to the service as soon as the request starts coming in? I've also asked on the WCF msdn forum here:

http://social.msdn.microsoft.com/Forums/en-US/wcf/thread/cfe625b2-1890-471b-a4bd-94373daedd39

August 5, 2009 2:07 PM

Leave a Comment

(required) 
(optional)
(required) 

  
Enter Code Here: Required
Submit

About carlosfigueira

Software Design Engineer in Test at the Connected Frameworks team at Microsoft.

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker