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