Przesyłanie dużego pliku do WCF z asp.net mvc 3

0

Cześć. Mam problem z przesłaniem dużego pliku do WCFa. Pogrzebałem dużo w Internecie, niestety nie znalazłem rozwiązania swojego problemu.
Sprawa wygląda tak: mam usługę WCF hostowaną na IIS 6.1, której zadaniem jest odebranie pliku. Mam również stronkę w asp.net mvc 3 (hostowaną na tym samym serwerze) z prostym formularzem umożliwiającym wybranie i wysłanie pliku. Całość działa na Windowsie 7 Enterprise, piszę w Visual Studio 2010 Pro.
Wszystko działa, jeżeli w kontrolerze przyjmuję żądanie i wywołuję usługę WCF w jednym wątku, jeżeli spróbuję to zrobić w dwóch, to dostaję ObjectDisposedException z treścią "Cannot access a closed file.". Ponadto problem występuje tylko dla plików większych niż 80kB, niestety nie mam pojęcia, gdzie mam błąd w konfiguracji. Będę wdzięczny za pomoc.
Poniżej kody źródłowe:
WCF:

 using System;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace TransferManager
{
    [ServiceContract]
    public interface ITransferService
    {
        [OperationContract(Action = "UploadFile", IsOneWay = false)]
        void UploadFile(RemoteFileInfo request);
    }

    [MessageContract]
    public class RemoteFileInfo : IDisposable
    {
        [MessageHeader(MustUnderstand = true)]
        public string FileName;

        [MessageHeader(MustUnderstand = true)]
        public long Length;

        [MessageBodyMember(Order = 1)]
        public System.IO.Stream FileByteStream;

        public void Dispose()
        {
            if (FileByteStream != null)
            {
                FileByteStream.Close();
                FileByteStream = null;
            }
        }   

    }
}

Implementacja interfejsu:

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

namespace TransferManager
{
    public class TransferService : ITransferService
    {
        private readonly Logger.Logger _logger = new Logger.Logger();

        public void UploadFile(RemoteFileInfo request)
        {
            _logger.Info("Receiving file");
            FileStream targetStream;
            var sourceStream = request.FileByteStream;

            string uploadFolder = WebConfigurationManager.AppSettings["UploadDirectory"];
            if(!Directory.Exists(uploadFolder))
            {
                Directory.CreateDirectory(uploadFolder);
            }
            string filePath = Path.Combine(uploadFolder, request.FileName);
            
            using (targetStream = new FileStream(filePath, FileMode.Create,FileAccess.Write))
            {
                const int bufferLen = 65536;
                var buffer = new byte[bufferLen];
                int count;
                while ((count = sourceStream.Read(buffer, 0, bufferLen)) > 0)
                {
                    targetStream.Write(buffer, 0, count);
                }
                sourceStream.Close();
            }
            _logger.Info("File received");
        }
    }
}
 

Kontroler

 using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Mvc;
using FileUploader.Models;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using FileUploader.ServiceReference1;

namespace FileUploader.Controllers
{
    public class HomeController : Controller
    {

        public ActionResult Index()
        {
            return View();
        }

        void progressStream_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            HttpContext.Application[e.Name] = 100 * e.BytesRead / e.Length;
        }

        [HttpPost]
        public string SendFile(HttpPostedFileBase file)
        {
            ThreadPool.QueueUserWorkItem(a => StartUploadingFile(file)); //**Jeżeli wywołam funkcję w ten sposób, to dostaję wyjątek.**
            //StartUploadingFile(file); //**Jeżeli zrobię to tak, to wszystko jest okej.**
            return "Done";
        }

        private void StartUploadingFile(HttpPostedFileBase postedFile)
        {
            if(postedFile == null)
            {
                return;
            }
            var client = new TransferServiceClient();
            var progressStream = new StreamWithProgress(postedFile.InputStream, postedFile.FileName);
            var name = Path.GetFileName(postedFile.FileName);
            progressStream.ProgressChanged += progressStream_ProgressChanged;
            client.UploadFile(name, postedFile.ContentLength, progressStream);

            HttpContext.Application[name] = 0;
        }

        [HttpPost]
        public long UploadingProgress(string filename)
        {
            if (HttpContext.Application.AllKeys.FirstOrDefault(a => a.Equals(filename)) == null)
            {
                return 0;
            }
            long returnValue = Convert.ToInt64(HttpContext.Application[filename]);
            if (returnValue == 100)
            {
                HttpContext.Application.Remove(filename);
            }
            return returnValue;
        }
    }
}

Ponadto używam własnego strumienia, aby móc kontrolować postęp wysyłania pliku

// Code written by Dimitris Papadimitriou - http://www.papadi.gr
// Code is provided to be used freely but without any warranty of any kind

using System;
using System.IO;

namespace FileUploader.Models
{
    public class StreamWithProgress : Stream
    {
        private readonly Stream _file;
        private readonly long _length;
        private readonly string _name;

        public event EventHandler<ProgressChangedEventArgs> ProgressChanged;

        private long _bytesRead;

        public StreamWithProgress(Stream file, string name)
        {
            _file = file;
            _length = file.Length;
            _bytesRead = 0;
            _name = name;
            if (ProgressChanged != null)
            {
                ProgressChanged(this, new ProgressChangedEventArgs(_bytesRead, _length, _name));
            }
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            int result = _file.Read(buffer, offset, count); // **Jeżeli wywołam usługę z drugiego wątku, to tutaj dostaję ObjectDisposedException z treścią "Cannot access a closed file."**
            _bytesRead += result;
            if (ProgressChanged != null)
            {
                ProgressChanged(this, new ProgressChangedEventArgs(_bytesRead, _length, _name));
            }
            return result;
        }

        public double GetProgress()
        {
            return ((double)_bytesRead) / _file.Length;
        }

        public override bool CanRead
        {
            get { return _file.CanRead; }
        }

        public override bool CanSeek
        {
            get { return _file.CanSeek; }
        }

        public override bool CanWrite
        {
            get { return _file.CanWrite; }
        }

        public override void Flush()
        {
            _file.Flush();
        }

        public override long Length
        {
            get { return _file.Length; }
        }

        public override long Position
        {
            get { return _bytesRead; }
            set { _file.Position = value; }
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            return _file.Seek(offset, origin);
        }

        public override void SetLength(long value)
        {
            _file.SetLength(value);
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            _file.Write(buffer, offset, count);
        }
    }
}
 

Poniżej configi. Najpierw config usługi WCF:

<?xml version="1.0"?>
<configuration>
  <appSettings>
    <add key="UploadDirectory" value="E:\upload\"/>
  </appSettings>
  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel" switchValue="Information, ActivityTracing" propagateActivity="true">
        <listeners>
          <add name="traceListener" type="System.Diagnostics.XmlWriterTraceListener" initializeData="D:\log\Traces.svclog"/>
        </listeners>
      </source>
    </sources>
  </system.diagnostics>
  <system.web>
    <compilation debug="true" targetFramework="4.0"/>
    <httpRuntime maxRequestLength="2147483647" useFullyQualifiedRedirectUrl="true" executionTimeout="14400"/>
  </system.web>
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="TransferService" closeTimeout="00:01:00" openTimeout="00:01:00" 
                 receiveTimeout="10:00:00" sendTimeout="00:01:00" allowCookies="true" 
                 maxBufferSize="2147483647" maxReceivedMessageSize="2147483647"
                 messageEncoding="Text" textEncoding="utf-8" 
                 transferMode="Streamed">
          <readerQuotas maxStringContentLength="2147483647" maxDepth="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
          <security mode="None"/>
        </binding>
      </basicHttpBinding>
    </bindings>
    <services>
      <service behaviorConfiguration="TransferServiceBehavior" name="TransferManager.TransferService">
        <endpoint address="" binding="basicHttpBinding" bindingConfiguration="TransferService" contract="TransferManager.ITransferService" isSystemEndpoint="false"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="TransferServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <security>
      <requestFiltering>
        <requestLimits maxAllowedContentLength="2147483647"/>
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>

A tutaj konfiguracja stronki

<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=152368
  -->
<configuration>
  <configSections>
    <sectionGroup name="elmah">
      <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
      <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
      <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
      <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
    </sectionGroup>
  </configSections>
  <connectionStrings>
    <add name="ApplicationServices" connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <appSettings>
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  </appSettings>
  <system.web>
    <customErrors mode="On" defaultRedirect="~/Error">
    </customErrors>
    <compilation debug="true" targetFramework="4.0">
      <assemblies>
        <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      </assemblies>
    </compilation>
    <httpRuntime maxRequestLength="2147483647" useFullyQualifiedRedirectUrl="true" executionTimeout="14400" />
    <authentication mode="Forms">
      <forms loginUrl="~/Account/LogOn" timeout="2880" />
    </authentication>
    <membership>
      <providers>
        <clear />
        <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="ApplicationServices" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />
      </providers>
    </membership>
    <profile>
      <providers>
        <clear />
        <add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="ApplicationServices" applicationName="/" />
      </providers>
    </profile>
    <roleManager enabled="false">
      <providers>
        <clear />
        <add name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="ApplicationServices" applicationName="/" />
        <add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />
      </providers>
    </roleManager>
    <pages>
      <namespaces>
        <add namespace="System.Web.Helpers" />
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
        <add namespace="System.Web.WebPages" />
      </namespaces>
    </pages>
    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" />
    </httpModules>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules runAllManagedModulesForAllRequests="true">
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    </modules>
    <security>
      <requestFiltering>
        <requestLimits maxAllowedContentLength="2147483647"></requestLimits>
      </requestFiltering>
    </security>
  </system.webServer>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="BasicHttpBinding_ITransferService" 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="Text" 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/TransferManager/TransferService.svc"
        binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_ITransferService"
        contract="ServiceReference1.ITransferService" name="BasicHttpBinding_ITransferService" />
    </client>
  </system.serviceModel>
  <elmah>
    <!--
        See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for 
        more information on remote access and securing ELMAH.
    -->
    <security allowRemoteAccess="true" />
  </elmah>
  <location path="elmah.axd" inheritInChildApplications="false">
    <system.web>
      <httpHandlers>
        <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
      </httpHandlers>
      <!-- 
        See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for 
        more information on using ASP.NET authorization securing ELMAH.

      <authorization>
        <allow roles="admin" />
        <deny users="*" />  
      </authorization>
      -->
    </system.web>
    <system.webServer>
      <handlers>
        <add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" />
      </handlers>
    </system.webServer>
  </location>
</configuration>

Będę wdzięczny za pomoc.
Pozdrawiam,
Afish

2

Problem rozwiązany. W sekcji httpRuntime w web.config stronki MVC trzeba było zwiększyć requestLengthDiskTreshold. Po zmianie cała sekcja wygląda tak:

<httpRuntime maxRequestLength="2147483647" useFullyQualifiedRedirectUrl="true" executionTimeout="14400"  requestLengthDiskThreshold="2147483647"/>

Teraz wszystko śmiga.
Pozdrawiam,
Afish

1 użytkowników online, w tym zalogowanych: 0, gości: 1