Extensions for System.Threading.Tasks.Task
.
Inspired by John Thiriet's blog posts: Removing Async Void and MVVM - Going Async With AsyncCommand.
Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices/
SafeFireAndForget
- An extension method to safely fire-and-forget a
Task
:
- An extension method to safely fire-and-forget a
WeakEventManager
- Avoids memory leaks when events are not unsubscribed
- Used by
AsyncCommand
andAsyncCommand<T>
- Usage instructions
-
Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices.MVVM/
-
Allows for
Task
to safely be used asynchronously withICommand
:IAsyncCommand : ICommand
AsyncCommand : IAsyncCommand
IAsyncCommand<T> : ICommand
AsyncCommand<T> : IAsyncCommand<T>
- Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices/
- Add to any project supporting .NET Standard 1.0
- Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices.MVVM/
- Add to any project supporting .NET Standard 2.0
Update Conference 2018, Correcting Common Mistakes in Asynchronous .NET Code: https://www.youtube.com/watch?v=yyT6dSjq-nE
Async/await is great but there are two subtle problems that can easily creep into code:
- Creating race conditions/concurrent execution (where you code things in the right order but the code executes in a different order than you expect)
- Creating methods where the compiler recognizes exceptions but you the coder never see them (making it head-scratchingly annoying to debug especially if you accidentally introduced a race condition that you can’t see)
This library solves both of these problems.
To better understand why this library was created and the problem it solves, it’s important to first understand how the compiler generates code for an async method.
And by the way, tl;dr A non-awaited Task
doesn't rethrow exceptions so use this library!
(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.
(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 return
s 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.
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!
Now we see that the async
method catches every exception thrown - that is to say, the exception is caught internally by the state machine, but you the coder will not see it. In order for you to see it, you'll need to rethrow the exception to surface it in your debugging. So the questions is - how do I rethrow the exception?
There are a few ways to rethrow exceptions that are thrown in an async
method:
- Use the
await
keyword (Prefered)- e.g.
await DoSomethingAsync()
- e.g.
- Use
.GetAwaiter().GetResult()
- e.g.
DoSomethingAsync().GetAwaiter().GetResult()
- e.g.
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.
Never, never, never, never, never use .Result
or .Wait()
:
-
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 theTask
has completed. -
.Result
or.Wait()
rethrow your exception as aSystem.AggregateException
, which makes it difficult to find the actual exception.
An extension method to safely fire-and-forget a Task
:
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.ToString()));
// 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, inspired by Xamarin.Forms.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));
readonly WeakEventManager<string> _errorOcurredEventManager = new WeakEventManager<string>();
public event EventHandler<string> ErrorOcurred
{
add => _errorOcurredEventManager.AddEventHandler(value);
remove => _errorOcurredEventManager.RemoveEventHandler(value);
}
public void RaiseErrorOcurred(string message) => _weakEventManager.HandleEvent(this, message, nameof(ErrorOcurred));
Allows for Task
to safely be used asynchronously with ICommand
:
AsyncCommand<T> : IAsyncCommand<T>
IAsyncCommand<T> : ICommand
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.ToString()));
ExampleAsyncCommandNotReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: false);
}
public IAsyncCommand ExampleAsyncCommand { get; }
public IAsyncCommand<int> 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);
}
}