Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WCF Asynchronous 'End' method not called after upgrading .NET

Tags:

c#

.net

wcf

upgrade

We have an application (console application "server" self hosting a bunch of WCF services and a few WPF clients) written in .NET 3.5. I wanted to have a go at upgrading the "server" app to .NET 4.6. For testing I was just going to change the runtime and add some child projects in 4.6, leaving the rest of the projects at 3.5. In the top level project I changed the target to 4.6 and made sure the app.config file had this in it:

<startup useLegacyV2RuntimeActivationPolicy="true">
  <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/>
</startup>

The WCF services and other support projects are in the solution and I did not modify them in anyway.

I also did not modify the WPF client.

In some of our services we implement the asynchronous begin/end pattern on the server. This is new to me as I learned WCF with the async/await pattern (though I'm familiar with begin/end in general). Any asynchronous requirements on the client are left up to the calling code.

Service

public interface IMyServiceCallback
{
    void UnrelatedNotifyClientsMethod(Notification message);
}



[ServiceContract(CallbackContract = typeof(IMyServiceCallback))]
public interface IMyService
{
    // ...

    [OperationContract(AsyncPattern = true)]
    IAsyncResult BeginFetchSomething(int howMany, AsyncCallback callback, object state);
    FetchSomethingResult EndFetchSomething(IAsyncResult result);

    // ...
}



[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class MyService : IMyService
{
    // ...

    [PermissionSetAttribute(SecurityAction.LinkDemand, Name = "FullTrust")]
    public IAsyncResult BeginFetchSomething(int howMany, AsyncCallback callback, object state)
    {
        AsyncResult<FetchSomethingResult> something = new AsyncResult<FetchSomethingResult>(callback, state);

        BackgroundWorker backgroundWorker = new BackgroundWorker();
        backgroundWorker.DoWork += new DoWorkEventHandler((sender, args) =>
        {
            try
            {
                FetchSomethingResult resultData = Database.FetchSomethingQuery(howMany);
                something.Result = resultData;
                something.Complete(true);
            }
            catch (Exception e)
            {
                Log(e);
                something.HandleException(e, false);
            }
            backgroundWorker.Dispose();
        });

        backgroundWorker.RunWorkerAsync();
        return something;
    }


    public FetchSomethingResult EndFetchSomething(IAsyncResult result)
    {
        AsyncResult<FetchSomethingResult> something = result as AsyncResult<FetchSomethingResult>;
        something.AsyncWaitHandle.WaitOne();
        return something.Result;
    }

    // ...

    // other similar methods

    // ...

}



public class AsyncResult : IAsyncResult
{

    // ...

    public void Complete(bool completedSynchronously)
    {
        lock (this.Mutex)
        {
            this.IsCompleted = true;
            this.CompletedSynchronously = completedSynchronously;
        }
        this.SignalCompletion();
    }

    protected void SignalCompletion()
    {
        (this.AsyncWaitHandle as ManualResetEvent).Set();
        ThreadPool.QueueUserWorkItem(d => { this.InvokeCallback(); });
    }

    protected void InvokeCallback()
    {
        if (this.Callback != null)
        {
            this.Callback(this);
        }
    }

    public void HandleException(Exception e, bool completedSynchronously)
    {
        lock (this.Mutex)
        {
            this.IsCompleted = true;
            this.CompletedSynchronously = completedSynchronously;
            this.Exception = e;
        }
        this.SignalCompletion();
    }

    // ...
}

Client (As generated by Visual Studio "Add Service Reference")

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
public partial class MyServiceClient : System.ServiceModel.DuplexClientBase<NameSpace.ServiceClients.IMyServiceClient>, NameSpace.ServiceClients.IMyServiceCliene
{
    // ...

    public NameSpace.ServiceClients.FetchSomethingResult FetchSomething(int howMany)
    {
        return base.Channel.FetchSomething(howMany);
    }

    // ...

}

On the client we have a few generic classes that wrap and expose our services, calls can happen on the main thread but usually are on background threads via background workers. By simply upgrading the server app from .NET 3.5 to 4+ and not making any changes, the End methods on the server are no longer called. I've confirmed that the Begin methods return and that the workers call .Complete() and invoke the callback, but nothing happens after that. After 1 minute the client will throw a timeout exception on the call.

Our codebase is fairly large and complex and I wasn't expecting any changes in behaviour after reading the .NET 4 migration notes.

EDIT: I've included a screenshot of the microsoft service trace utility showing the same call to FetchProductVersion before the update (top, 3.5) and after (bottom, 4.6).

WCF async pattern end method not called

EDIT 2/3: The failures seem inconsistent in some cases.

like image 211
plast1k Avatar asked Nov 01 '17 14:11

plast1k


1 Answers

You must properly set IAsyncResult.CompletedSynchronously property to false because you complete IAsyncResult in asynchronous manner. In contrary to WCF in .NET Framework 4.6, WCF in .NET Framework 3.5 just ignores it in this case.

So change something.Complete(true); to something.Complete(completedSynchronously: false);.

You can check who and when uses IAsyncResult.CompletedSynchronously by placing a breakpoint in it's getter:

bool _completedSynchronously = false;
public bool CompletedSynchronously
{
    get
    {
        // Set breakpoint here.
        return _completedSynchronously;
    }
    set
    {
        _completedSynchronously = value;
    }
}

If you launch WCF service using .NET Framework 3.5 you will notice that it is never actually called. So older version of WCF simply ignores it. If you launch service using .NET Framework 4.6 you will see that it is used quite a lot by WCF internal class DispatchOperationRuntime. By InvokeBegin() and InvokeCallback() functions in particular.

like image 75
Leonid Vasilev Avatar answered Sep 29 '22 20:09

Leonid Vasilev