Udostępnij za pośrednictwem


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();
}
}

Comments

  • Anonymous
    September 23, 2008
    The comment has been removed
  • Anonymous
    December 22, 2008
    I've put a new release up on codeplex. This release includes an implementation of Atom…
  • Anonymous
    August 05, 2009
    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
  • Anonymous
    December 28, 2009
    Carlos,Thanks for the snippet (and your efforts in general - your blog is a great resource!).I'm having a few problems with this one though. For starters, I noticed that - although streaming is configured for my services - my service method is not hit unless the GetResponse method is being invoked, causing the client to gather the whole data in memory.The resolution seems to be to set the AllowWriteStreamBuffering to false (SendChunked didn't help at all), but strange enough, this completely breaks the sample. While the memory consumption on the client stays low this time, the service method is still not hit (which bears the question what happens with the streamed data), and the client finally causes an exception as soon as GetResponse is being invoked:System.Net.WebException: This request requires buffering data to succeed.I tried to fiddle with the parameters but given my service is not invoked anyway until GetResponse (rather than every time I write to the request stream), I think the problem starts earlier than with GetResponse.Any advice on this?
  • Anonymous
    January 10, 2010
    jimblust, could you provide your changes tolet this sniped be deployed under iis. I have the sample running as a console application, but deploying it to IIS hasn't been successfully. I am getting a bad request.
  • Anonymous
    March 13, 2010
    if we host the service on IIS we should change the host tag to look like this<%@ ServiceHost Language="C#" Debug="true" Service="SADADNET.Services.Receiver.RouterService" Factory="System.ServiceModel.Activation.WebServiceHostFactory"%>or if you are using Windsor to Activate you service it should look like this<%@ ServiceHost Language="C#" Debug="true" Service="SADADNET.Services.Receiver.RouterService" Factory="Castle.Facilities.WcfIntegration.WindsorServiceHostFactory`1[[Castle.Facilities.WcfIntegration.Rest.RestServiceModel,Castle.Facilities.WcfIntegration]], Castle.Facilities.WcfIntegration"%>
  • Anonymous
    June 30, 2010
    The comment has been removed
  • Anonymous
    July 20, 2010
    @Mitani, try using Bare (not WrappedRequest) so you'll be able to receive any XML (including SOAP) requests in your operation. WebInvoke should work in this case. But at this point you'll have to parse the XML yourself (since you're using REST, not SOAP) and decide what to return to the caller.
  • Anonymous
    October 17, 2010
    The comment has been removed
  • Anonymous
    October 19, 2010
    The comment has been removed
  • Anonymous
    November 16, 2010
    I get this error when i open the host:For request in operation UploadFile to be a stream the operation must have a single parameter whose type is Stream...can some one please let me know if it is possible to have more than one parameter in your operation if you have stream as one 0f the parameters
  • Anonymous
    November 20, 2010
    vicky, do you get this error when you open the host, or when you browse to the service address? If the latter, you may be hitting a known bug in WCF (the metadata generation code in WCF isn't aware of the special case for Stream parameters composed with UriTemplate ones). If the former, make sure that all non-Stream parameters in the operation are bound to the UriTemplate - the WCF runtime will treat all of them as URL parameters (i.e., extract them from the request URI), and deliver all the request body to the Stream parameter.
  • Anonymous
    July 25, 2011
    Hi, So is there a way to POST XML to a WCF REST service in .NET 4.0 or should we still use raw streams? Also if raw stream is used to parse this data: <User><Id>SomeID</Id></User> , can this data be then mapped or "deserialized" back into an entity (for e.g. the following)?public class User{ public string Id { get; set;}}
  • Anonymous
    July 25, 2011
    WCFRestUser, if you want to receive XML, then you don't need to use raw streams - WCF has some built-in support for it. If you want to work in an untyped way, you can just replace the Stream parameter with an XElement (or XmlElement), and the whole request body will be mapped to it:[OperationContract]void ReceiveXml(XElement input);Or you can also map it to a "strong" type. In your case, you'd have something like[WebInvoke][XmlSerializerFormat]void ReceiveXml(User User);
  • Anonymous
    July 25, 2011
    Thanks for the Response. I have been trying to write a simple WCF REST POST using VS 2008 SP1 using Jmeter for GET and POST without any luck. The GET works but the POST fails.Assuming Sevice.svc is wired to the ServiceHandler.This works:GET http://domainname/Service.svc/usersreturns:<User xmlns="ns"><ID>MyId</ID>..</User>This returns 400 Bad Request. I am unable to hit the breakpoint on the AddNewUser method.POST http://domainname/Service.svc/usersPOST Data:<User xmlns="ns"><ID>MyId</ID>..</User>========================================================[CollectionDataContract(Name = "Users", Namespace = "ns")]   public class Users : List<User>   {   }   [DataContract(Name = "User", Namespace = "")]   public class User   {       [DataMember(Name = "ID", Order = 1)]       public string m_Id;       [DataMember(Name = "FirstName", Order = 2)]       public string m_FirstName;       [DataMember(Name = "LastName", Order = 3)]       public string m_LastName;       [DataMember(Name = "Email", Order = 4)]       public string m_Email;   }   [ServiceContract]   public interface IServiceHandler   {       [OperationContract]       [WebGet(UriTemplate = "/users", BodyStyle = WebMessageBodyStyle.Bare, ResponseFormat = WebMessageFormat.Xml)]       [Description("Gets All the Users.")]       Users GetAllUsers();       [OperationContract]       [WebInvoke(UriTemplate = "/users",           BodyStyle = WebMessageBodyStyle.Bare,           RequestFormat = WebMessageFormat.Xml,           ResponseFormat = WebMessageFormat.Xml,           Method = "POST")]       [Description("Creates a new User.")]       User AddNewUser(User user);   }[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]   public sealed class ServiceHandler : IServiceHandler   {private static Users s_Users       {           get           {               return new Users()           {               new User() { m_Email ="Test1@test1.com", m_FirstName="FirstNaam1", m_Id="FirstId1", m_LastName="LastNAam1"},               new User() { m_Email ="Test2@test2.com", m_FirstName="FirstNaam2", m_Id="FirstId2", m_LastName="LastNAam2"}           };           }       }       public Users GetAllUsers()       {           return s_Users;       }public User AddNewUser(User user)       { return null;}}
  • Anonymous
    August 02, 2011
    If you get a "Bad Request", you should enable tracing at the server (msdn.microsoft.com/.../ms733025.aspx). The traces will have an exception explaining why the request was considered to be "bad".
  • Anonymous
    August 10, 2011
    I will try to enable the tracing. On a different note, what happens when we want products by both Id and Name? The scenarios below will give WCF 500 errors as these will resolve to the same URITemplates, right?/Products/{id}/Products/{name}Even the querystrings wont work in this case:/Products?id={id}/Products?name={name}
  • Anonymous
    August 10, 2011
    In this case you'll need to either have a different template ("/Products/{id}" and "/ProductsByName/{name}), or have both on the query string (/Products?id={id}&name={name}). In the latter case, you'd choose how to filter the products depending on which parameter is populated (any parameter not passed to the query string will be null for ref types).The query string option which you mentioned (/Products?id={id} and /Products?name={name}) doesn't work because the operation for a request such as http://.../Products (no query string) would be ambiguous.
  • Anonymous
    August 10, 2011
    Thanks. I was thinking of either using this (notice the "Products" and "Product")/Products?id={id}/Product?name={name}OR use this:/Products/ProductId/{Id}/Products/ProductName/{name}Is there any design standard on this? For e.g. when to use a QS and when not to. I know the important thing would be to be consistent.
  • Anonymous
    August 10, 2011
    Since the ID is (I suppose) some sort of primary key for the product, I've seen most cases where you have/Product/{id}Since this is the location of the product resource. Using a name is equivalent to a search operation in the resource collection of products, so I've seen things like/Products?name={name}But I don't know whether there is a cut and dry standard for this.
  • Anonymous
    August 10, 2011
    Thanks so much for this! Life saving work! (well, not quite, but close!)Cheers,-Nic
  • Anonymous
    October 09, 2011
    I thought this would be incredibly simple, but I must be missing something. I am trying to make a simple WCF POST request in conjunction with a UriTemplate. I have read numerous examples where people use a stream paramater as the last paramater, and this is supposed to pick up the POST body. I can only get this to work if the stream is the only paramater.I've gone back to basics with a simple Hello World service.Here is my code on the client   static string Test()   {       HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create("http://localhost:1884/MyAPI/Service.svc/HelloWorld");       req.Method = "POST";       req.ContentType = "text/plain";       Stream reqStream = req.GetRequestStream();       byte[] fileToSend = System.Text.UTF8Encoding.UTF8.GetBytes("sometext");       reqStream.Write(fileToSend, 0, fileToSend.Length);       reqStream.Close();       HttpWebResponse resp = (HttpWebResponse)req.GetResponse();       var sr = new StreamReader(resp.GetResponseStream());       return sr.ReadToEnd();   }And this is the code on the service   [ServiceContract]   public interface IService   {       [OperationContract]       [WebInvoke(Method = "POST", UriTemplate = "HelloWorld")]       Stream HelloWorld(Stream content);   }   public Stream HelloWorld(Stream content)   {       var sr = new StreamReader(content);       string text = sr.ReadToEnd();       return new System.IO.MemoryStream(Encoding.UTF8.GetBytes("Hello World! " + text));   }This all works fine. Then I make this change:   [ServiceContract]   public interface IService   {       [OperationContract]       [WebInvoke(Method = "POST", UriTemplate = "HelloWorld/test/{test}")]       Stream HelloWorld(string test, Stream content);   }   public Stream HelloWorld(string test, Stream content)   {       var sr = new StreamReader(content);       string text = sr.ReadToEnd();       return new System.IO.MemoryStream(Encoding.UTF8.GetBytes("Hello World! " + text + test));   }And change the client code to hit HelloWorld/test/sometextI get a 500 Internal Server Error. I've tried about 10 different variations including using a ?key=value type UriTemplate, returning strings instead of streams etc, and no luck.Feels like I'm missing some tiny little thing that is going to make this work, as I have seen countless examples of exactly this all over the web. Theirs works, mine doesn't.Any ideas?
  • Anonymous
    October 09, 2011
    The comment has been removed
  • Anonymous
    October 10, 2011
    Hi James, I've created some code at http://codepaste.net/s5hwyv and http://codepaste.net/xtx3ho similar to your scenario and it worked out fine; take a look at the samples, and see if there is anything different from your code.
  • Anonymous
    October 10, 2011
    Success!I created a new WCF service, and copied all the code over from the old one. I remember this was the most common fix to unsolvable code mysteries back in my Uni days. Gotta love Visual Studio.Thanks for your help Carlos. Glad that 4 day headache has come to an end.
  • Anonymous
    October 26, 2011
    hi all , can i upload the zip file ?
  • Anonymous
    October 27, 2011
    You can upload zip files as well; as far as WCF is concerned, it's just a bunch of bytes coming over the wire, it doesn't care what format those bytes are in.
  • Anonymous
    December 12, 2011
    Is there a way to enable raw request processing for individual OperationContracts?In general, I'm okay with using the DataContractJsonSerializer, but for some objects that are passed in as parameters to the OperationContact methods, I need to use JSON.NET. The problem is that in these cases, I don't want the client to have be aware that it should be setting Content-Type: text/plain instead of application/json (which is used in every other case).
  • Anonymous
    December 15, 2011
    When trying this, I get a blank output from IE.When running fiddler, I can see the response properly, but I have noticed the following:The Content-Type reported is application/xml instead the value I have forced throughreq.ContentType = "application/octet-stream".Any suggestion? thanks in advance...;
  • Anonymous
    February 28, 2012
    Hi Konstantin P, this setting is done at the encoder level, so that's not trivial to have a per-operation content-type mapper. You can still implement it, though, and I've done so at github.com/.../PerOperationContentTypeMapper (I'll write a post about it tomorrow, it will be online at blogs.msdn.com/.../supporting-raw-requests-for-individual-operations.aspx when it's published).
  • Anonymous
    February 28, 2012
    Hi Jorge, I don't understand your problem. In the example shown in the post, the operation doesn't return anything, so it's expected that you won't see any response from the browser, but you'll still get the HTTP headers, which is likely what you see in Fiddler. Is that the case?
  • Anonymous
    May 15, 2013
    The comment has been removed
  • Anonymous
    September 15, 2013
    i am getting " For request in operation RecieveImage to be a stream the operation must have a single parameter whose type is Stream error. " while passing parameters with post url...but it is working if pass the stream and it is saving the image....I am unable to paste my code due to text length.Here i have posted my question please check it..stackoverflow.com/.../unable-to-upload-an-image-with-parameters-to-wcf-rest-serviceorsocial.msdn.microsoft.com/.../unable-to-upload-an-image-with-parameters-to-wcf-rest-service
  • Anonymous
    October 14, 2014
    Please help me regarding following issse.By adding Factory ,I got to know issue with endpoint because . I am getting error as "Error in endpoint" .Code[WebInvoke(Method = "POST",          ResponseFormat = WebMessageFormat.Json,          RequestFormat = WebMessageFormat.Json,          BodyStyle = WebMessageBodyStyle.Wrapped)]       string Test(string msg);public string Test(string msg)       {           return msg;       }Config<configuration> <system.web>   <compilation debug="true" targetFramework="4.0" />   <customErrors mode="Off"/> </system.web> <system.serviceModel>   <behaviors>     <endpointBehaviors>       <behavior name="httpBehavior">         <webHttp />         <dataContractSerializer maxItemsInObjectGraph="2147483647" />       </behavior>     </endpointBehaviors>     <serviceBehaviors>       <behavior name="NotifyServiceBehavior">         <serviceMetadata httpGetEnabled="true"/>         <serviceDebug includeExceptionDetailInFaults="true"/>       </behavior>     </serviceBehaviors>   </behaviors>   <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />   <bindings>     <webHttpBinding>       <binding name="webHttpBindingWithJson" crossDomainScriptAccessEnabled="true" maxBufferSize="2147483647" maxReceivedMessageSize="2147483647">         <readerQuotas
                     &nbsp;maxDepth=&quot;2147483647&quot;                 &nbsp;maxStringContentLength=&quot;2147483647&quot;                 &nbsp;maxArrayLength=&quot;2147483647&quot;                 &nbsp;maxBytesPerRead=&quot;2147483647&quot;                 &nbsp;maxNameTableCharCount=&quot;2147483647&quot; /&gt;
             <security mode="None" />       </binding>     </webHttpBinding>   </bindings>   <services>     <service name="Notify.Service1" behaviorConfiguration="NotifyServiceBehavior">       <endpoint address="" behaviorConfiguration="httpBehavior" bindingConfiguration="webHttpBindingWithJson" binding="webHttpBinding" contract="Notify.IService1" bindingName="LargeWeb" />     </service>   </services> </system.serviceModel> <system.webServer>   <modules runAllManagedModulesForAllRequests="true"/>   <httpProtocol>     <customHeaders>       <add name="Access-Control-Allow-Origin" value="*" />       <add name="Access-Control-Allow-Headers" value="Content-Type" />     </customHeaders>   </httpProtocol> </system.webServer></configuration>