4

I'm kinda stuck here...

My goal is quite simple: I want to expose an IIS hosted (and then Windows Azure) WCF service through which I can upload files, using streaming, and add some META data about the file I want to upload (filename, MD5-hash all the usual stuff...), and to be able to display accurate progress information regarding the upload.

First of all I created a derived class StreamWithProgress which inherits from FileStream, where I have overridden the Read method to raise an event with each read through which I pass progress information.

Secondly I created a WCF service using a MessageContract ( http://msdn.microsoft.com/en-us/library/ms730255.aspx ) to wrap the META data, and the stream object into a single SOAP envelope. This service is really simple, exposing only a single method for upload.

I have set all the buffer sizes to accept large amounts of data, as per:

and the httpRuntime settings as per:

the IIS\ASP Compatibility settings as per:

And disabling batching as per:

I have created a self hosted service through which the upload succeeded. Then I ‘upgraded’ it to an IIS hosted service (on my local machine), which worked. Then I created a locally hosted Windows Azure service, with a WCF webrole, which worked.

The catch though is that in none of the instances did actual streaming take place… All of them buffered the data before sending it.

I came across this problem, when I saw that my client is reporting progress, but the server doesn’t start writing the file until after the entire file was buffered.

My actual code follows.

Any ideas\help? Anything will be greatly appreciated…

Thanks!

Server web.config:

<?xml version="1.0"?>
<configuration>
    <system.serviceModel>

        <bindings>
            <basicHttpBinding>
                <binding name="uploadBasicHttpBinding" 
                 maxReceivedMessageSize="2147483647" 
                 transferMode="Streamed" 
                 messageEncoding="Mtom"
                 maxBufferPoolSize="2147483647"
                 maxBufferSize="2147483647">
                 <readerQuotas maxArrayLength="2147483647" 
                                maxBytesPerRead="2147483647" 
                                maxDepth="2147483647" 
                                maxNameTableCharCount="2147483647" 
                                maxStringContentLength="2147483647"/>
                </binding>
            </basicHttpBinding>
        </bindings>

            <behaviors>
                <serviceBehaviors>
                    <behavior name="defaultBehavior">
                        <serviceMetadata httpGetEnabled="true"/>
                        <serviceDebug includeExceptionDetailInFaults="false"/>
                        <dataContractSerializer maxItemsInObjectGraph="2147483647"/>
                    </behavior>
                </serviceBehaviors>
            </behaviors>

        <!-- Add this for BufferOutput setting -->
        <serviceHostingEnvironment multipleSiteBindingsEnabled="true" aspNetCompatibilityEnabled="true"/>

        <services>
            <service name="WcfService1.Service1" behaviorConfiguration="defaultBehavior">           
                <endpoint binding="basicHttpBinding" contract="WcfService1.IService1" bindingConfiguration="uploadBasicHttpBinding"/>
                <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
            </service>
        </services>

    </system.serviceModel>

    <system.webServer>
        <modules runAllManagedModulesForAllRequests="true"/>
    </system.webServer>

    <system.web>
        <compilation debug="true"/>
    <httpRuntime maxRequestLength="2147483647" />
    </system.web>

</configuration>

Service Contract:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.IO;

namespace WcfService1
{
    [ServiceContract]
    public interface IService1
    {
        [OperationContract(IsOneWay=true)]
        void UploadStream(Encapsulator data);
    }
}

Actual Service:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;

using System.IO;
using System.Web;
using System.ServiceModel.Activation;

namespace WcfService1
{
    [MessageContract]
    public class Encapsulator
    {
        [MessageHeader(MustUnderstand = true)]
        public string fileName;
        [MessageBodyMember(Order = 1)]
        public Stream requestStream;
    }

    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class Service1 : IService1
    {
        public Service1()
        {
            HttpContext context = HttpContext.Current;

            if (context != null)
            {
                context.Response.BufferOutput = false;
            }
        }

        public void UploadStream(Encapsulator data)
        {
            const int BUFFER_SIZE = 1024;

            int bytesRead = 0;

            byte[] dataRead = new byte[BUFFER_SIZE];

            string filePath = Path.Combine(@"C:\MiscTestFolder", data.fileName);

            string logPath = Path.Combine(@"C:\MiscTestFolder", string.Concat(data.fileName, ".log"));

            bytesRead = data.requestStream.Read(dataRead, 0, BUFFER_SIZE);

            StreamWriter logStreamWriter = new StreamWriter(logPath);

            using (System.IO.FileStream fileStream = new System.IO.FileStream(filePath, FileMode.Create))
            {
                while (bytesRead > 0)
                {
                    fileStream.Write(dataRead, 0, bytesRead);
                    fileStream.Flush();

                    logStreamWriter.WriteLine("Flushed {0} bytes", bytesRead.ToString());
                    logStreamWriter.Flush();

                    bytesRead = data.requestStream.Read(dataRead, 0, BUFFER_SIZE);
                }

                fileStream.Close();
            }

            logStreamWriter.Close();
        }
    }
}

Client app.config:

<?xml version="1.0"?>
<configuration>
    <system.serviceModel>

        <bindings>
            <basicHttpBinding>
                <binding name="BasicHttpBinding_IService1" closeTimeout="00:01:00"
                    openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
                    allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
                    maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
                    messageEncoding="Mtom" textEncoding="utf-8" transferMode="Streamed"
                    useDefaultWebProxy="true">
                    <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                        maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                    <security mode="None">
                        <transport clientCredentialType="None" proxyCredentialType="None"
                            realm="" />
                        <message clientCredentialType="UserName" algorithmSuite="Default" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>

        <client>
            <endpoint address="http://localhost/WcfService1/Service1.svc"
                binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1"
                contract="UploadService.IService1" name="BasicHttpBinding_IService1" />
        </client>

    </system.serviceModel>

    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
    </startup>
</configuration>

Client Main code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using CustomFileUploaderTester.UploadService;
using System.ServiceModel;
using System.IO;

namespace CustomFileUploaderTester
{
    class Program
    {
        private static long bytesRead = 0;

        static void Main(string[] args)
        {
            Service1Client client = new Service1Client();

            using (StreamWithProgress fstream = new StreamWithProgress(@"C:\BladieBla\someFile.wmv", FileMode.Open))
            {
                client.InnerChannel.AllowOutputBatching = false;

                fstream.ProgressChange += new EventHandler<StreamReadProgress>(fstream_ProgressChange);

                client.UploadStream("someFile.wmv", fstream);

                fstream.Close();
            }

            Console.ReadKey();
        }

        static void fstream_ProgressChange(object sender, StreamReadProgress e)
        {
            bytesRead += e.BytesRead;

            Console.WriteLine(bytesRead.ToString());
        }
    }
}

Derived FileStream Class (StreamWithProgress)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace CustomFileUploaderTester
{
    public class StreamReadProgress : EventArgs
    {
        #region Public Properties

        public long BytesRead
        {
            get;
            set;
        }

        public long Length
        {
            get;
            set;
        }

        #endregion

        #region Constructor

        public StreamReadProgress(long bytesRead, long fileLength)
            : base()
        {
            this.BytesRead = bytesRead;

            this.Length = fileLength;
        }

        #endregion
    }

    public sealed class StreamWithProgress : FileStream
    {
        #region Public Events

        public event EventHandler<StreamReadProgress> ProgressChange;

        #endregion

        #region Constructor

        public StreamWithProgress(string filePath, FileMode fileMode)
            : base(filePath, fileMode)
        {
        }

        #endregion

        #region Overrides

        public override int Read(byte[] array, int offset, int count)
        {
            int bytesRead = base.Read(array, offset, count);

            this.RaiseProgressChanged(bytesRead);

            return bytesRead;
        }

        #endregion

        #region Private Worker Methods

        private void RaiseProgressChanged(long bytesRead)
        {
            EventHandler<StreamReadProgress> progressChange = this.ProgressChange;

            if (progressChange != null)
            {
                progressChange(this, new StreamReadProgress(bytesRead, this.Length));
            }
        }


        #endregion
    }
}

-- Update: 2012-04-20

After I have installed a loop-back adapter, I traced the comms with RawCap, and saw that the data is actually streamed, but that the IIS server is buffering all the data before invoking the web method!

According to this post:

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

it's ASP.Net behavior that WCF inherits... But they're talking about fixes for this in .Net 4.5 :|

If anyone has any other suggestion it will be great!

Thanks!!

Community
  • 1
  • 1
mnemonic
  • 692
  • 1
  • 9
  • 18
  • Does it make any difference if you lower the server side MaxBufferSize property? – RichBower Apr 19 '12 at 15:23
  • Hi RichBower, I tried that thanks... After your post I went and played around with all the ReaderQuota, and Binding settings... but to no avail... – mnemonic Apr 20 '12 at 07:14

2 Answers2

3

You’re using Mtom together with streamed transfer mode. This may cause issues. Please try to remove Mtom. Actually, Mtom is a very old standard anyway. In addition, when using SOAP services with streamed transfer mode, we can only have a single parameter whose type is Stream. We cannot use a custom type like Encapsulator.

The recommended solution to build file upload/download services is to use REST. One of the ways to build REST services on .NET platform is to use ASP.NET Web API: http://www.asp.net/web-api. Using this API, we don’t need to deal with streaming transfer mode. What we need to deal with is the Range header. This blog post may help: http://blogs.msdn.com/b/codefx/archive/2012/02/23/more-about-rest-file-upload-download-service-with-asp-net-web-api-and-windows-phone-background-file-transfer.aspx. But please also note this API is not released yet. If you don't want to use pre-release products, you can use other technologies, for example, you can use MVC controller as a REST service, or use WCF REST service, or build a custom http handler, etc. If you want to use Stream, a custom stream is needed. I would like to suggest you to check http://blogs.msdn.com/b/james_osbornes_blog/archive/2011/06/10/streaming-with-wcf-part-1-custom-stream-implementation.aspx for a sample.

Best Regards,

Ming Xu.

Ming Xu - MSFT
  • 2,116
  • 1
  • 11
  • 13
  • Hi Ming, Thanks for the reply! Unfortunately using REST is not an option for us, but thanks for the references! I'm sure it will come in handy eventually! :) – mnemonic Apr 24 '12 at 14:05
3

After some rigorous testing, I saw that data is actually being streamed. I applied the [MessageContract] attribute to the Encapsulator class (as per http://msdn.microsoft.com/en-us/library/ms733742.aspx), and that enabled me to send some extra Meta data about the file. Using WireShark and RawCap is was clear that data was sent over the wire while the Stream was read.

The other problem that stuck his head out, was that the data being streamed is buffered server-side (using IIS 7.5) before the upload method is actually invoked! This is a bit of a concern, but according to this: http://social.msdn.microsoft.com/Forums/is/wcf/thread/cfe625b2-1890-471b-a4bd-94373daedd39, a fix should be in the 4.5 release of .Net

mnemonic
  • 692
  • 1
  • 9
  • 18