Partager via


Using TransferMode.StreamedResponse to download files in Silverlight 4

After seeing some questions about this new feature added in Silverlight 4, I decided to post an example to see how it can be used in some real world scenarios. This new mode was added mostly to enable the performance improvements in the Polling Duplex protocol, but it can also be used by itself, such as to download a large file from a WCF service. The whole project for this blog post can be downloaded here.

A simple download service contract looks like the one below.

    [ServiceContract(Namespace = "")]
    public interface IWcfDownloadService
    {
        [OperationContract]
        Stream Download(string fileName, long fileSize);
    }

This works because WCF (on the desktop) treats the “System.IO.Stream” type as a special case (the “stream programming model”). In this programming model, one can return any subclass of Stream (FileStream, MemoryStream, etc), from that operation, and WCF will read from that stream and return the data to the client, in a way doing a special “serialization” of the stream contents. On the client side, if an operation returns a stream, the client can simply read from the Stream provided by WCF and it will return the bytes that were sent by the server.

This is all good on the desktop world, but on Silverlight (as of SL4), however, this programming model is not available (remember, WCF in SL is a subset of the WCF in the desktop framework). When you do an “Add Service Reference” (ASR) in a SL project to a service which has an operation with a stream, the wizard will generate an equivalent operation, with the Stream parameter (or return value) replaced by byte[]. That’s because a Stream, as it’s treated by WCF, is simply a sequence of bytes, and the two contracts can be used interchangeably.

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(Namespace="", ConfigurationName="ServiceReference1.IWcfDownloadService")]
public interface IWcfDownloadService {
    [System.ServiceModel.OperationContractAttribute(AsyncPattern=true, Action="SOME_URL", ReplyAction="SOME_OTHER_URL")]
    System.IAsyncResult BeginDownload(string fileName, long fileSize, System.AsyncCallback callback, object asyncState);
    byte[] EndDownload(System.IAsyncResult result);
}

The only problem with this “solution” is that while on a streaming case the client can read the bytes from the stream as they are arriving from the service, with a byte[] return type WCF will first read the whole stream to create a byte[] object, then it will hand it over to the user code. If this file being transferred is really big, that can (and often will) be a problem (too much memory being consumed in the client). There is, however, a solution on where one can do “real” streaming in Silverlight, although the code has to go down to the message level. I’ll show step by step what needs to be done to have it working in Silverlight.

First, since the operation in Silverlight will be untyped (i.e., its input and output will be of type “System.ServiceModel.Channels.Message”, which is sort of “object” in WCF-speak), it’s often easier to redefine the Action and ReplyAction properties of the operation contract. This way we don’t have to know what is the value that WCF generated when creating the Message object. Also, even though it’s possible to have an equivalent interface defined in the client project, I’ve found it useful to centralize the definition of the interfaces, and then use some compile-time directives to split the differences between the desktop version of the interface and the SL version of it – the file will be physically present in one of the projects, and linked from the other. The contract below is the original contract, modified to be used both in SL and in the server projects:

namespace SLApp.Web
{
    [ServiceContract(Namespace = "")]
    public interface IWcfDownloadService
    {
#if !SILVERLIGHT
        [OperationContract(Action = Constants.DownloadAction, ReplyAction = Constants.DownloadReplyAction)]
        Stream Download(string fileName, long fileSize);
#else
        [OperationContract(AsyncPattern = true, Action = Constants.DownloadAction, ReplyAction = Constants.DownloadReplyAction)]
        IAsyncResult BeginDownload(Message request, AsyncCallback callback, object state);
        Message EndDownload(IAsyncResult asyncResult);
#endif
    }

    public static class Constants
    {
        public const string DownloadAction = "https://my.company.com/download";
        public const string DownloadReplyAction = "https://my.company.com/download";
    }
}

Since this is not the original client created by the ASR dialog, we can add to the config file the information required to access the endpoint with that new interface (additions to the original config are in bold).

<configuration>
    <system.serviceModel>
        <bindings>
            <customBinding>
              <binding name="CustomBinding_IWcfDownloadService">
                <binaryMessageEncoding />
                <httpTransport maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" />
              </binding>
              <binding name="CustomBinding_IWcfDownloadService_StreamedResponse">
<binaryMessageEncoding />
<httpTransport maxReceivedMessageSize="2147483647"
maxBufferSize="2147483647"
transferMode="StreamedResponse" />
</binding>

            </customBinding>
        </bindings>
        <client>
          <endpoint address="https://localhost:9160/WcfDownloadService.svc"
              binding="customBinding" bindingConfiguration="CustomBinding_IWcfDownloadService"
              contract="ServiceReference1.IWcfDownloadService" name="CustomBinding_IWcfDownloadService" />
          <endpoint address=" https://localhost:9160/WcfDownloadService.svc"
              binding="customBinding" bindingConfiguration="CustomBinding_IWcfDownloadService_StreamedResponse"
contract="SLApp.Web.IWcfDownloadService" name="CustomBinding_IWcfDownloadService_StreamedResponse" />

        </client>
    </system.serviceModel>
</configuration>

We also need a way to create a body with the same schema expected by the service. This is fairly simple, we just need a [DataContract] type whose order of the data members is the same order as the operation parameters. Also, the name and namespace of the contract have to match the ones for the service contract:

[DataContract(Name = "Download", Namespace = "")] // same namespace as the [ServiceContract], same name as operation
public class DownloadRequest
{
    [DataMember(Order = 1)]
    public string fileName;
    [DataMember(Order = 2)]
    public long fileSize;
}

In addition, the client created by ASR doesn’t implement this interface, so the easier way at this point is to use a ChannelFactory<T> to create the proxy used to talk to the service:

private void btnStartStreaming_Click(object sender, RoutedEventArgs e)
{
    string endpointName = "CustomBinding_IWcfDownloadService_StreamedResponse";
    ChannelFactory<SLApp.Web.IWcfDownloadService> factory = new ChannelFactory<Web.IWcfDownloadService>(endpointName);
    SLApp.Web.IWcfDownloadService proxy = factory.CreateChannel();
    SLApp.Web.DownloadRequest request = new SLApp.Web.DownloadRequest();
    request.fileName = "test.bin";
    request.fileSize = 1000000000L; // ~1GB
    Message input = Message.CreateMessage(factory.Endpoint.Binding.MessageVersion, SLApp.Web.Constants.DownloadAction, request);
    proxy.BeginDownload(input, new AsyncCallback(this.DownloadCallback), proxy);
    this.AddToDebug("Called proxy.BeginDownload");
}

Finally, when we receive the callback for the operation, we must read the message body as a XmlDictionaryReader; the reader is capable of reading the message in a real streamed way. By using the ReadContentAsBase64(byte[], int, int) method you can treat it essentially as a Stream object (and it’s Read(byte[], int, int) method).

void DownloadCallback(IAsyncResult asyncResult)
{
    SLApp.Web.IWcfDownloadService proxy = (SLApp.Web.IWcfDownloadService)asyncResult.AsyncState;
    this.AddToDebug("Inside DownloadCallback");
    try
    {
        Message response = proxy.EndDownload(asyncResult);
        this.AddToDebug("Got the response");
        if (response.IsFault)
        {
            this.AddToDebug("Error in the server: {0}", response);
        }
        else
        {
            XmlDictionaryReader bodyReader = response.GetReaderAtBodyContents();
            if (!bodyReader.ReadToDescendant("DownloadResult")) // Name of operation + "Result"
            {
                this.AddToDebug("Error, could not read to the start of the result");
            }
            else
            {
                bodyReader.Read(); // move to content
                long totalBytesRead = 0;
                int bytesRead = 0;
                int i = 0;
                byte[] buffer = new byte[1000000];
                do
                {
                    bytesRead = bodyReader.ReadContentAsBase64(buffer, 0, buffer.Length);
                    totalBytesRead += bytesRead;
                    i++;
                    if ((i % 100) == 0)
                    {
                        this.AddToDebug("Read {0} bytes", totalBytesRead);
                    }
                } while (bytesRead > 0);

                this.AddToDebug("Read a total of {0} bytes", totalBytesRead);
            }
        }
    }
    catch (Exception e)
    {
        this.AddToDebug("Exception: {0}", e);
    }
}

And that should do it :)

Again, a VS 2010 solution with this code can be downloaded here. You can run it and monitor the memory usage in the machine. The service is returning 1GB of data to the client; if streamed wasn’t used, you should observe at least a 1GB increase in the memory usage. In this case, since “real” streaming is being used, the memory increase is a lot less.

Comments

  • Anonymous
    October 11, 2010
    This is a great academic example, but has anyone actually put this into practice? I have integrated this functionality into my existing Silverlight/WCF business app. I am using it as a way to supplement my current WCF function of loading lists of business objects that come from the DB and passing them as DTOs across WCF. I now have 2 different services: my regular one that includes a WCF function called LoadList (returning a list of DTOs), and this new one that has a function called LoadListStream (that calls my other method, serializes the list of DTOs into xml, then bytes, and then returns it as a stream. On the other side I use the ReadContentAsBase64 call. When I do that, I invoke a callback that updates a progressbar on the UI. Actually to make this usable I had to include the length of the stream being returned in the HTTP headers so I could compute a percentage of how far along I was. Anyway, the point is that in the end, this is very slow. I don't know if I am doing something wrong but when I make this call to pull down about 2.5Mb of data on a DSL speed connection, it takes about 70 seconds to complete. If I use Fiddler I can capture the raw data that was sent, save it as a text file and place that in a folder on the server. When I attempt to use the browser to pull down that file in a URL, it takes about 17 seconds to download. I cannot figure out what the hold up is. All I can find is that the call to ReadContentAsBase64 takes about 1 second in between each all to it. Everything else is lightning quick, but it always holds up for about 1 second at that call, if it is on a slower connection, it holds even longer. The only time it does not take about 1 second on that call is if the WCF service and the Silverlight app are running on the same LocalHost. If that is the case, it goes incredibly quick. Please let me know if you have experienced anything different.
  • Anonymous
    November 18, 2010
    This is a great example. I downloaded but it required the clientaccesspolicy / crossdomain.xml to run without a security error.I am looking to reverse this with an Upload stream. Is there any limitation that you can see with opening a Stream on the silverlight side to upload a large file?
  • Anonymous
    March 04, 2011
    Great article. I was wondering - as well as E.Pahl - what are the limitations of the inverse process- Uploading a file as a stream to SL 4-.
  • Anonymous
    March 15, 2011
    WCF in Silverlight doesn't support streaming on the upload direction (the two values for the TransferMode enumeration are Buffered and StreamedResponse), so you'd actually need to use HttpWebRequest directly to do that. If you search for "silverlight upload file" (www.bing.com/search) you'll find some solutions for this problem.
  • Anonymous
    March 17, 2011
    I'm trying to implement this in my project, I'm running up against a problem where the asyncResult.AsyncState in DownloadCallback is not an instance of the IWcfDownloadService (or my equivalent), but rather is coming in as a BodyWriterMessage. I've been through all my code and config and everything looks correctly wired up. The only difference I can see is that the service reference looks a lot different. Did you generate them using the VS 2010 "Add Service Reference" or did you generate them using a different tool?I'd be grateful for any help.
  • Anonymous
    July 12, 2011
    I tested the solution and it's working great for small files.But for larger files, the DownloadCallback function doesn't fire or else this error occurs :"The server did not provide a meaningful reply; this might be caused by a contract mismatch, a premature session shutdown or an internal server error."My limit os size before getting the error is about 415Mb in localhost and 25Mb on server.I added all necessary parameters for the binding in web.config like maxBufferSize etc...I would be very very interesting to fix this issue, so please do not hesitate to answer !
  • Anonymous
    July 26, 2011
    Etienne, can you enable tracing on the server to see if there is anything at that side which is aborting the connection?
  • Anonymous
    January 27, 2012
    The comment has been removed
  • Anonymous
    January 27, 2012
    Already solved - my fault :-). Thanks for your article!!!
  • Anonymous
    January 28, 2012
    Harry, great you solved you problem! Thanks.
  • Anonymous
    January 28, 2012
    I tried to implement this in my project but getting an error when try to accessbytesRead = bodyReader.ReadContentAsBase64(buffer, 0, buffer.Length);System.NotSupportedException was unhandled by user code Message=Read is not supported on the main thread when buffering is disabled. StackTrace:      at MS.Internal.InternalNetworkStream.Read(Byte[] buffer, Int32 offset, Int32 count)      at System.ServiceModel.Channels.PreReadStream.Read(Byte[] buffer, Int32 offset, Int32 count)      at System.ServiceModel.Channels.HttpInput.WebResponseHttpInput.WebResponseInputStream.Read(Byte[] buffer, Int32 offset, Int32 count)      at System.ServiceModel.Channels.MaxMessageSizeStream.Read(Byte[] buffer, Int32 offset, Int32 count)      at System.ServiceModel.Channels.DetectEofStream.Read(Byte[] buffer, Int32 offset, Int32 count)      at System.Xml.XmlTextReaderImpl.ReadData()      at System.Xml.XmlTextReaderImpl.ParseText(Int32& startPos, Int32& endPos, Int32& outOrChars)      at System.Xml.XmlTextReaderImpl.ReadContentAsBinary(Byte[] buffer, Int32 index, Int32 count)      at System.Xml.XmlTextReaderImpl.ReadContentAsBase64(Byte[] buffer, Int32 index, Int32 count)      at System.Xml.XmlDictionaryReader.XmlWrappedReader.ReadContentAsBase64(Byte[] buffer, Int32 offset, Int32 count)
  • Anonymous
    January 30, 2012
    Hi brijen, do you get the same error if you try to run the sample code? I just posted the full VS project to the MSDN Samples gallery at code.msdn.microsoft.com/Using-TransferModeStreamedR-5b1fa6ed (and to github at github.com/.../StreamedResponses), so you can try starting from that example and modify the code until you get to the point where yours either work or you know exactly in which point the failure starts happening, which will be easier to diagnose then.
  • Anonymous
    September 27, 2012
    I run the sample program, it throws exception after 262017279 bytes read, very consistently.Could you help? I appreciate it.Read 262017279 bytesException: System.Xml.XmlException: Unexpected end of file. Following elements are not closed: DownloadResult, DownloadResponse, Body, Envelope.  at System.Xml.XmlExceptionHelper.ThrowXmlException(XmlDictionaryReader reader, String res, String arg1, String arg2, String arg3)  at System.Xml.XmlExceptionHelper.ThrowUnexpectedEndOfFile(XmlDictionaryReader reader)  at System.Xml.XmlBufferReader.ReadBytes(Int32 count)  at System.Xml.XmlBinaryReader.ReadText(XmlTextNode textNode, ValueHandleType type, Int32 length)  at System.Xml.XmlBinaryReader.ReadPartialBinaryText(Boolean withEndElement, Int32 length)  at System.Xml.XmlBinaryReader.ReadNode()  at System.Xml.XmlBinaryReader.Read()  at System.Xml.XmlBaseReader.ReadContentAsBase64(Byte[] buffer, Int32 offset, Int32 count)  at SLApp.MainPage.DownloadCallback(IAsyncResult asyncResult)
  • Anonymous
    March 21, 2013
    The same errorException: System.Xml.XmlException: Unexpected end of file. Following elements are not closed: DownloadResult, DownloadResponse, Body, Envelope.  at System.Xml.XmlExceptionHelper.ThrowXmlException(XmlDictionaryReader reader, String res, String arg1, String arg2, String arg3)  at System.Xml.XmlExceptionHelper.ThrowUnexpectedEndOfFile(XmlDictionaryReader reader)  at System.Xml.XmlBufferReader.ReadBytes(Int32 count)  at System.Xml.XmlBinaryReader.ReadText(XmlTextNode textNode, ValueHandleType type, Int32 length)  at System.Xml.XmlBinaryReader.ReadPartialBinaryText(Boolean withEndElement, Int32 length)  at System.Xml.XmlBinaryReader.ReadNode()  at System.Xml.XmlBinaryReader.Read()  at System.Xml.XmlBaseReader.ReadContentAsBase64(Byte[] buffer, Int32 offset, Int32 count)  at SLApp.MainPage.DownloadCallback(IAsyncResult asyncResult)Happens only in IEPlease advise
  • Anonymous
    January 15, 2014
    I have the same errorException: System.Xml.XmlException: Unexpected end of file. Following elements are not closed: DownloadResult, DownloadResponse, Body, Envelope. at System.Xml.XmlExceptionHelper.ThrowXmlException(XmlDictionaryReader reader, String res, String arg1, String arg2, String arg3) at System.Xml.XmlExceptionHelper.ThrowUnexpectedEndOfFile(XmlDictionaryReader reader) at System.Xml.XmlBufferReader.ReadBytes(Int32 count) at System.Xml.XmlBinaryReader.ReadText(XmlTextNode textNode, ValueHandleType type, Int32 length) at System.Xml.XmlBinaryReader.ReadPartialBinaryText(Boolean withEndElement, Int32 length) at System.Xml.XmlBinaryReader.ReadNode() at System.Xml.XmlBinaryReader.Read() at System.Xml.XmlBaseReader.ReadContentAsBase64(Byte[] buffer, Int32 offset, Int32 count) at SLApp.MainPage.DownloadCallback(IAsyncResult asyncResult)
  • Anonymous
    January 30, 2014
    Hi I too facing the same exception.. its happening in IE only