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.