7

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

 3 years ago
source link: https://blog.ndepend.com/migrating-delegate-begininvoke-calls-to-net-core-net-5-and-net-6/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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:

   var asyncResult = new Func<int, int>(FuncDouble).MyBeginInvoke(11);
   ... // Do some work before calling TryEndInvoke()
   Assert.IsTrue(asyncResult.TryEndInvoke(out int result, out Exception ex));
   Assert.IsTrue(result == 22);
private int FuncDouble(int a) { return a + a; }

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.
   public interface IMyAsyncResult<TResult> {
      bool TryEndInvoke(out TResult result, out Exception exception);
   public static partial class AsyncDelegateExtensionMethods {
      // First scenario, gather a IMyAsyncResult<T> object on which TryEndInvoke() can be called
      internal static IMyAsyncResult<TResult> MyBeginInvoke<T0, TResult>(
               this Func<T0, TResult> func, T0 arg0) {
         MyAssert.IsNotNull(func);
         Task<TResult> task = Task.Run(() => func.Invoke(arg0));
         return new MyAsyncResult<TResult>(task);
      // Such MyBeginInvoke() method overload is required for each Func<> cardinality, below with 3 arguments
      public static IMyAsyncResult<TResult> MyBeginInvoke<T0, T1, T2, TResult>(
         this Func<T0, T1, T2, TResult> func, T0 arg0, T1 arg1, T2 arg2) {
         MyAssert.IsNotNull(func);
         Task<TResult> task = Task.Run(() => func.Invoke(arg0, arg1, arg2));
         return new MyAsyncResult<TResult>(task);
      internal const string ERR_MSG = "TryEndInvoke() has already been called on this object";
      sealed class MyAsyncResult<TResult> : IMyAsyncResult<TResult> {
         private readonly Task<TResult> m_Task;
         internal MyAsyncResult(Task<TResult> task) {
            MyAssert.IsNotNull(task);
            m_Task = task;
         private bool m_Disposed;
         bool IMyAsyncResult<TResult>.TryEndInvoke(out TResult result, out Exception exception) {
            if(m_Disposed) { throw new InvalidOperationException(ERR_MSG);}
            m_Disposed = true;
            TaskAwaiter<TResult> awaiter = m_Task.GetAwaiter();
               result = awaiter.GetResult();
               exception = null;
               return true;
            } catch (Exception ex) {
               result = default;
               exception = ex;
               return false;
            } finally {
               m_Task.Dispose();

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.

   [TestFixture]
   public partial class Test_AsyncDelegateExtensionMethods {
      [SetUp]
      public void SetUp() {
         TestHelper.SetUpTests();
      private int m_ManagedThreadId;
      private int m_ManagedThreadIdAsync;
      [Test]
      public void Test_BeginInvoke_With_AsyncResult_OK() {
         m_ManagedThreadId = Thread.CurrentThread.ManagedThreadId;
         var asyncResult = new Func<int, int>(FuncDouble).MyBeginInvoke(11);
         Assert.IsTrue(asyncResult.TryEndInvoke(out int result, out Exception ex));
         Assert.IsTrue(result == 22);
         Assert.IsNull(ex);
         // Cannot call TryEndInvoke() a second time!
         bool exThrown = false;
            asyncResult.TryEndInvoke(out result, out ex);
         } catch (InvalidOperationException invEx) {
            Assert.IsTrue(invEx.Message == AsyncDelegateExtensionMethods.ERR_MSG);
            exThrown = true;
         Assert.IsTrue(exThrown);
      private int FuncDouble(int a) {
         Assert.IsTrue(m_ManagedThreadId != Thread.CurrentThread.ManagedThreadId);
         return a + a;
      [Test]
      public void Test_BeginInvoke_With_AsyncResult_KO() {
         m_ManagedThreadId = Thread.CurrentThread.ManagedThreadId;
         var asyncResult = new Func<int, int>(FuncEx).MyBeginInvoke(11);
         Assert.IsFalse(asyncResult.TryEndInvoke(out int result, out Exception ex));
         Assert.IsTrue(result == default);
         Assert.IsTrue(ex.Message == "Hello");
      private int FuncEx(int a) {
         Assert.IsTrue(m_ManagedThreadId != Thread.CurrentThread.ManagedThreadId);
         throw new ArgumentException("Hello");
      [Test]
      public void Test_BeginInvoke3Args_With_AsyncResult_OK() {
         m_ManagedThreadId = Thread.CurrentThread.ManagedThreadId;
         var asyncResult = new Func<int, int, int, int>(Func3Arg).MyBeginInvoke(1, 2, 3);
         Assert.IsTrue(asyncResult.TryEndInvoke(out int result, out Exception ex));
         Assert.IsTrue(result == 6);
         Assert.IsNull(ex);
      private int Func3Arg(int a, int b, int c) {
         Assert.IsTrue(m_ManagedThreadId != Thread.CurrentThread.ManagedThreadId);
         return a + b + c;

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.

new Func<int, int>(FuncDouble).MyBeginInvoke(11,
           // The task completed procedure that consumes the result
           (int result, Exception ex) => {
              if(ex == null) {
                 // Ok task executed properly, now we can do something with the result
                 Console.WriteLine($"Result {result}");
              } else { ... }

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

   public static partial class AsyncDelegateExtensionMethods {
      // Second scenario: do some action on task completed instead of calling EndInvoke on an IAsyncResult-like object
      internal static void MyBeginInvoke<T0, TResult>(
            this Func<T0, TResult> func, T0 arg0,
            Action<TResult, Exception> onTaskCompleted) {
         MyAssert.IsNotNull(func);
         MyAssert.IsNotNull(onTaskCompleted);
         Task<TResult> task = Task.Run(() => func.Invoke(arg0));
         ContinueOnTaskCompleted(task, onTaskCompleted);
      private static void ContinueOnTaskCompleted<TResult>(
            Task<TResult> task,
            Action<TResult, Exception> onTaskCompleted) {
         MyAssert.IsNotNull(task);
         MyAssert.IsNotNull(onTaskCompleted);
         Task unused = task.ContinueWith(
            (Task<TResult> taskTmp) => {
                  if (taskTmp.IsFaulted) {
                     AggregateException aggregateEx = taskTmp.Exception;
                     MyAssert.IsNotNull(aggregateEx);
                     Exception ex = aggregateEx.InnerExceptions.Single();
                     onTaskCompleted(default, ex);
                     return;
                  MyAssert.IsTrue(taskTmp.IsCompleted);
                  onTaskCompleted(taskTmp.Result, null);
               } finally {
                  task.Dispose();
                  taskTmp.Dispose();

Here are the tests that fully cover this implementation.

[TestFixture]
public partial class Test_AsyncDelegateExtensionMethods {
   // FuncDouble() / FuncEx() / m_ManagedThreadId are defined in the test code above
   [Test]
   public void Test_MyBeginInvoke_With_OnTaskCompleted_OK() {
      m_ManagedThreadId = Thread.CurrentThread.ManagedThreadId;
      var @event = new ManualResetEvent(false);
      new Func<int, int>(FuncDouble).MyBeginInvoke(11,
         (int result, Exception ex) => {
            Assert.IsTrue(m_ManagedThreadId != Thread.CurrentThread.ManagedThreadId);
            Assert.IsTrue(result == 22);
            Assert.IsNull(ex);
            @event.Set();
      @event.WaitOne();
   [Test]
   public void Test_MyBeginInvoke_With_OnTaskCompleted_KO() {
      m_ManagedThreadId = Thread.CurrentThread.ManagedThreadId;
      var @event = new ManualResetEvent(false);
      new Func<int, int>(FuncEx).MyBeginInvoke(11,
         (int result, Exception ex) => {
            Assert.IsTrue(m_ManagedThreadId != Thread.CurrentThread.ManagedThreadId);
            Assert.IsTrue(result == default);
            Assert.IsTrue(ex.Message == "Hello");
            @event.Set();
      @event.WaitOne();

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.

My dad being an early programmer in the 70's, I have been fortunate to switch from playing with Lego, to program my own micro-games, when I was still a kid. Since then I never stop programming.

I graduated in Mathematics and Software engineering. After a decade of C++ programming and consultancy, I got interested in the brand new .NET platform in 2002. I had the chance to write the best-seller book (in French) on .NET and C#, published by O'Reilly (> 15.000 copies) and also did manage some academic and professional courses on the platform and C#.

Over the years, I gained a passion for understanding structure and evolution of large complex real-world applications, and for talking with talented developers behind it. As a consequence, I got interested in static code analysis and started the project NDepend.

Today, with more than 8.000 client companies, including many of the Fortune 500 ones, NDepend offers deeper insight and understanding about their code bases to a wide range of professional users around the world.

I live with my wife and our twin babies Léna and Paul, in the beautiful island of Mauritius in the Indian Ocean.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK