Skip to content

sciseo/AsyncAwaitBestPractices

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

AsyncAwaitBestPractices

Build Status

Extensions for System.Threading.Tasks.Task.

Inspired by John Thiriet's blog posts: Removing Async Void and MVVM - Going Async With AsyncCommand.

  • AsyncAwaitBestPractices
    • SafeFireAndForget
      • An extension method to safely fire-and-forget a Task:
    • WeakEventManager
      • Avoids memory leaks when events are not unsubscribed
      • Used by AsyncCommand and AsyncCommand<T>
    • Usage instructions
  • AsyncAwaitBestPractices.MVVM
    • Allows for Task to safely be used asynchronously with ICommand:
      • IAsyncCommand : ICommand
      • AsyncCommand : IAsyncCommand
      • AsyncCommand<T> : IAsyncCommand
    • Usage instructions

Setup

AsyncAwaitBestPractices

NuGet NuGet

AsyncAwaitBestPractices.MVVM

NuGet NuGet

Why Do I Need This?

tl;dr A non-awaited Task doesn't rethrow exceptions

To understand why this library was created, it's important to first understand how the compiler generates code for an async method.

Compiler-Generated Code for Async Method

Compiler-Generated Code for Async Method

(Source: Xamarin University: Using Async and Await)

The compiler transforms an async method into an IAsyncStateMachine class which allows the .NET Runtime to "remember" what the method has accomplished.

Move Next

(Source: Xamarin University: Using Async and Await)

The IAsyncStateMachine interface implements MoveNext(), a method the executes every time the await operator is used inside of the async method.

MoveNext() essentially runs your code until it reaches an await statement, then it returns while the await'd method executes. This is the mechanism that allows the current method to "pause", yielding its thread execution to another thread/Task.

Try/Catch in MoveNext()

Look closely at MoveNext(); notice that it is wrapped in a try/catch block.

Because the compiler creates IAsyncStateMachine for every async method and MoveNext() is always wrapped in a try/catch, every exception thrown inside of an async method is caught!

How to Rethrow an Exception Caught By MoveNext

Now the question becomes, if every async method catches every exception thrown, How can I rethrow the exception?

There are a few ways to rethrow exceptions that are thrown in an async method:

  1. Use the await keyword (Prefered)
    • e.g. await DoSomethingAsync()
  2. Use .GetAwaiter().GetResult()
    • e.g. DoSomethingAsync().GetAwaiter().GetResult()

The await keyword is preferred because await allows the Task to run asynchronously on a different thread, and it will not lock-up the current thread.

What About .Result or .Wait()?

Never, never, never, never, never use .Result or .Wait():

  1. Both .Result and .Wait() will lock-up the current thread. If the current thread is the Main Thread (also known as the UI Thread), your UI will freeze until the Task has completed. 2..Result or .Wait() rethrow your exception as a System.AggregateException, which makes it difficult to find the actual exception.

Usage

AsyncAwaitBestPractices

An extension method to safely fire-and-forget a Task:

  • SafeFireAndForget
public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, bool continueOnCapturedContext = true, System.Action<System.Exception> onException = null)
void HandleButtonTapped(object sender, EventArgs e)
{
    // Allows the async Task method to safely run on a different thread while not awaiting its completion
    // If an exception is thrown, Console.WriteLine
    ExampleAsyncMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex.Message));

    // HandleButtonTapped continues execution here while `ExampleAsyncMethod()` is running on a different thread
    // ...
}

async Task ExampleAsyncMethod()
{
    await Task.Delay(1000);
}

An event implementation that enables the garbage collector to collect an object without needing to unsubscribe event handlers:

  • WeakEventManager
readonly WeakEventManager _weakEventManager = new WeakEventManager();

public event EventHandler CanExecuteChanged
{
    add => _weakEventManager.AddEventHandler(value);
    remove => _weakEventManager.RemoveEventHandler(value);
}

public void RaiseCanExecuteChanged() => _weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));

AsyncAwaitBestPractices.MVVM

Allows for Task to safely be used asynchronously with ICommand:

  • AsyncCommand<T> : IAsyncCommand
  • AsyncCommand : IAsyncCommand
  • IAsyncCommand : ICommand
public AsyncCommand(Func<T, Task> execute,
                     Func<object, bool> canExecute = null,
                     Action<Exception> onException = null,
                     bool continueOnCapturedContext = true)   
public AsyncCommand(Func<Task> execute,
                     Func<object, bool> canExecute = null,
                     Action<Exception> onException = null,
                     bool continueOnCapturedContext = true)
public class ExampleClass
{
    public ExampleClass()
    {
        ExampleAsyncCommand = new AsyncCommand(ExampleAsyncMethod);
        ExampleAsyncIntCommand = new AsyncCommand<int>(ExampleAsyncMethodWithIntParameter);
        ExampleAsyncExceptionCommand = new AsyncCommand(ExampleAsyncMethodWithException, onException: ex => Console.WriteLine(ex.Message));
        ExampleAsyncCommandNotReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: false);
    }

    public IAsyncCommand ExampleAsyncCommand { get; }
    public IAsyncCommand ExampleAsyncIntCommand { get; }
    public IAsyncCommand ExampleAsyncExceptionCommand { get; }
    public IAsyncCommand ExampleAsyncCommandNotReturningToTheCallingThread { get; }

    async Task ExampleAsyncMethod()
    {
        await Task.Delay(1000);
    }
  
    async Task ExampleAsyncMethodWithIntParameter(int parameter)
    {
        await Task.Delay(parameter);
    }

    async Task ExampleAsyncMethodWithException()
    {
        await Task.Delay(1000);
        throw new Exception();
    }

    void ExecuteCommands()
    {
        ExampleAsyncCommand.Execute(null);
        ExampleAsyncIntCommand.Execute(1000);
        ExampleAsyncExceptionCommand.Execute(null);
        ExampleAsyncCommandNotReturningToTheCallingThread.Execute(null);
    }
}

Packages

No packages published

Languages

  • C# 100.0%