NDepend Blog

Improve your .NET code quality with NDepend

Migrating Delegate.BeginInvoke Calls to .NET Core, .NET 5 and .NET 6

May 26, 2021 3 minutes read

Migrating Delegate.BeginInvoke Calls to .NET Core, .NET 5 and .NET 6

In this 2019 post, the .NET Base Class Library engineers announced that the good old Delegate.BeginInvoke .NET Framework syntax wasn’t supported in .NET Core and consequently in .NET 5, 6 … The reasons for this are twofold:

  1. The Task-based Asynchronous Pattern (TAP) is the recommended async model as of .NET Framework 4.5.
  2. The implementation of async delegates depends on remoting features not present in .NET Core,

The detailed discussion about this decision can be found here https://github.com/dotnet/runtime/issues/16312.

In the same post a code sample is provided to achieve the Delegate.BeginInvoke features with the TAP and the async/await keywords. I don’t find this solution satisfying because it requires significant refactoring for porting some Delegate.BeginInvoke calls to .NET Core.

Let’s see how we can implement the two primary Delegate.BeginInvoke usage scenarios with .NET Standard code. The key is to keep a syntax similar enough to simplify the migration from .NET Fx to .NET Core/5/6.

Scenario 1: Call EndInvoke() and on IAsyncResult object returned by BeginInvoke()

Here is the syntax we can achieve with the code below:

Find below the whole implementation followed by unit tests that 100% test it. Here are some remarks:

  • Ideally we would have wanted a syntax like FuncDouble.MyBeginInvoke(11) but there is no C# delegate type inference at call site. Thus we need to use new Func<int, int>(FuncDouble).MyBeginInvoke(11) instead.
  • A trivial MyBeginInvoke() overload is needed for each Func<T0,...,TN,Result> cardinality N.
  • I’ve never been a fan of Delegate.EndInvoke() re-throwing an exception thrown while executing the asynchronous procedure. Thus IMyAsyncResult<TResult> presents a bool TryEndInvoke(out TResult result, out Exception exception) method that returns false and the exception when failed. This discards the need for a try{ } catch{ } clause when calling EndInvoke().
  • TryEndInvoke() is called on the IMyAsyncResult object and not on the delegate object as originally. This syntax is easier since only one object needs to be provided instead of two to conclude the async task.
  • The key of this implementation is the private nested class MyAsyncResult<TResult> that keeps a reference to the async task and waits for termination upon a TaskAwaiter object.
  • Finally, notice that TryEndInvoke() can be called only once. An InvalidOperationException is thrown the second time it is called.

Here are the tests that challenge all possible paths. Thanks to some usage of Thread.CurrentThread.ManagedThreadId these tests check that the asynchronous procedure is actually executed on a background thread.

Scenario 2: Call an On-Task-Completed-Action once the asynchronous call is terminated

The second usual scenario to achieve is to provide an On-Task-Completed-Action to consume the result – or eventually the exception thrown – instead of calling an EndInvoke() method.

Here is the code to achieve that. The astute is to rely on Task.ContinueWith().

Here are the tests that fully cover this implementation.

Conclusion

Migrating .NET Fx calls to Delegate.BeginInvoke() to .NET Core .NET 5/6 can be tricky. Hopefully the code provided in this post greatly simplifies this task.