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:
http://msdn.microsoft.com/en-us/library/e1f13641(v=vs.71).aspx and
http://kjellsj.blogspot.com/2007/02/wcf-streaming-upload-files-over-http.html
the IIS\ASP Compatibility settings as per:
And disabling batching as per:
- http://msdn.microsoft.com/en-us/library/system.servicemodel.icontextchannel.allowoutputbatching.aspx
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!!