Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WCF - Abort stream from client to server if server throws a FaultException

Tags:

.net

vb.net

wcf

We have a WCF service that accepts a stream from a client (client uploading a file to server). However, if the server throws a FaultException before or during the stream the client just carries on streaming until the end regardless, at which point it receives the FaultException from the server - wasting time & bandwidth for the client.

Similar Question:

How to Abort a WCF File Upload from the Server-Side Method

Take the following (simplified WCF service)

Namespace JP_WCF
    <ServiceContract> _
    Public Interface IJP_WCF
        <OperationContract> _
        <FaultContract(GetType(JP_WCF_Fault))> _
        Sub UploadFile(request As JP_WCF_FileUpload)

        <OperationContract> _
        <FaultContract(GetType(JP_WCF_Fault))> _
        Function fakeError(ByVal int1 As Integer, ByVal int2 As Integer) As Integer

        <OperationContract> _
        <FaultContract(GetType(JP_WCF_Fault))> _
        Function Ping() As Date
    End Interface

    <MessageContract> _
    Public Class JP_WCF_FileUpload
        Implements IDisposable

        <MessageHeader(MustUnderstand:=True)> _
        Public FileName As String

        <MessageHeader(MustUnderstand:=True)> _
        Public Length As Long

        <MessageBodyMember(Order:=1)> _
        Public FileByteStream As System.IO.Stream

        Public Sub Dispose() Implements IDisposable.Dispose
            If FileByteStream IsNot Nothing Then
                FileByteStream.Close()
                FileByteStream = Nothing
            End If
        End Sub
    End Class

    <DataContract> _
    Public Class JP_WCF_Fault
        <DataMember> _
        Public Property EventID() As Integer
        <DataMember> _
        Public Property Message() As String
        <DataMember> _
        Public Property Description() As String

        Public Sub New(ByVal _EventID As Integer, ByVal _Message As String, ByVal _Description As String)
            Me.EventID = _EventID
            Me.Message = _Message
            Me.Description = _Description
        End Sub
    End Class

End Namespace

Example Server method:

Try
    Dim sourceStream As Stream = request.FileByteStream
    Dim uploadFolder As String = "C:\upload\"
    Dim filePath As String = Path.Combine(uploadFolder, request.FileName)

    Using targetStream = New FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)
        sourceStream.CopyTo(targetStream)
        targetStream.Close()
        sourceStream.Close()
    End Using
Catch ex As Exception
    Throw New FaultException(Of JP_WCF_Fault)(New JP_WCF_Fault(8, ex.Message, ex.ToString), ex.Message)
End Try

Example Client Method:

Dim fileInfo As New System.IO.FileInfo(filePath)
Dim startTime As DateTime = DateTime.Now
Console.WriteLine("Starting V2 upload: " + DateTime.Now.ToString())

Dim JPCS As New JP_WCFService.JP_WCFClient()

Using stream As New System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read)
    Using uploadStreamWithProgress As New JP_StreamWithProgress(stream)
        AddHandler uploadStreamWithProgress.ProgressChanged, AddressOf uploadStreamWithProgress_ProgressChanged
        Try
            JPCS.UploadFile(fileInfo.Name, fileInfo.Length, uploadStreamWithProgress)
        Catch ex As FaultException(Of JP_WCFService.JP_WCF_Fault)
            Console.WriteLine("Upload Error: " & ex.Detail.Message & " (EventID: " & ex.Detail.EventID.ToString & ")")
        End Try
    End Using
End Using
Dim endTime As DateTime = DateTime.Now
Dim durationInMS As Double = (endTime - startTime).TotalMilliseconds
Console.WriteLine(vbCr & "V2 Upload Completed: " + DateTime.Now.ToString() + " (" + durationInMS.ToString() + ")")
JPCS.Close()

web.config

<system.serviceModel>
    <bindings>
        <customBinding>
            <binding name="JP_WCFBinding">
                <!-- maxReceivedMessageSize 600MB, maxBufferSize 2MB -->
                <binaryMessageEncoding compressionFormat="GZip" />
                <httpsTransport transferMode="Streamed" maxReceivedMessageSize="629145600" maxBufferSize="2097152"/>
            </binding>
        </customBinding>
    </bindings>
    <services>
        <service behaviorConfiguration="JP_WCFbehavior" name="JP_WCF.JP_WCFServices">
            <endpoint address="" binding="customBinding" bindingConfiguration="JP_WCFBinding" contract="JP_WCF.IJP_WCF"/>
        </service>
    </services>
    <behaviors>
        <serviceBehaviors>
            <behavior name="JP_WCFbehavior">
                <serviceMetadata httpGetEnabled="false" httpsGetEnabled="true" />
                <serviceDebug includeExceptionDetailInFaults="true" />
            </behavior>
        </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
</system.serviceModel>

app.config

 <system.serviceModel>
    <bindings>
        <customBinding>
            <binding name="CustomBinding_IJP_WCF">
                <binaryMessageEncoding compressionFormat="GZip" />
                <httpsTransport transferMode="Streamed" />
            </binding>
        </customBinding>
    </bindings>
    <client>
        <endpoint address="https://dev-wcf.localhost/JP_WCF.svc"
            binding="customBinding" bindingConfiguration="CustomBinding_IJP_WCF"
            contract="JP_WCFService.IJP_WCF" name="CustomBinding_IJP_WCF" />
    </client>
</system.serviceModel>
like image 206
HeavenCore Avatar asked Sep 27 '17 14:09

HeavenCore


2 Answers

If you're concerned about the performance of this call, you could always make a server call to check the validity of this upload prior to streaming it. That way you can avoid streaming the file at all if there's a problem and avoid any exception state in your application (also expensive).

So you would make relatively quick trip to the server to validate things like

  1. valid file location
  2. permission to write to that location
  3. the size of the file being valid
  4. any relevant business rules

Then you can make your call without trying to manage application flow using exceptions. Remember: Exceptions should be for exceptional circumstances. This way if your application does raise an exception, it means that something very abnormal has happened and a dip in speed is more palatable (since this case would be theoretically very rare).

like image 138
Tracy Moody Avatar answered Sep 28 '22 03:09

Tracy Moody


Do your clients have a duplex channel? If so, then it is quite straight forward to callback on the client contract to send information as the file is uploaded.

If not, then one good approach here would be to stream your data to the server in chunks using a memory buffer. There are a few good examples I've put below.

The the gist is that you split the file up into chunks on the client and send it chunk-by-chunk to the server. If any chunk fails, then it can either retry that chunk, or gracefully fail without sending any more data.

These references make use of a StreamWithProgress class to handle this logic on the client. Hope this helps.

Code Project reference

Simple implementation using StreamWithProgress class

like image 40
leon.io Avatar answered Sep 28 '22 03:09

leon.io