Extensions for System.Threading.Tasks.Task and System.Threading.Tasks.ValueTask
See the codeExtensions for System.Threading.Tasks.Task.
Inspired by John Thiriet's blog posts:
Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices/
SafeFireAndForget
Task or a ValueTaskTask will rethrow an Exception if an Exception is caught in IAsyncStateMachine.MoveNext()WeakEventManager
AsyncCommand, AsyncCommand<T>, AsyncValueCommand, AsyncValueCommand<T>Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices.MVVM/
Allows for Task to safely be used asynchronously with ICommand:
IAsyncCommand : ICommandAsyncCommand : IAsyncCommandIAsyncCommand<T> : ICommandAsyncCommand<T> : IAsyncCommand<T>IAsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute>AsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute, TCanExecute>Allows for ValueTask to safely be used asynchronously with ICommand:
IAsyncValueCommand : ICommandAsyncValueCommand : IAsyncValueCommandIAsyncValueCommand<T> : ICommandAsyncValueCommand<T> : IAsyncValueCommand<T>IAsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute>AsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute, TCanExecute>The author of this library teaches a hands-on async/await and parallel programming workshop for C# teams (taught at NDC London and NDC Oslo in 2026) and offers async audits of production code. Details and pricing: codetraveler.io/workshops and codetraveler.io/dotnet-consulting.
Join me in these DomeTrain courses where we'll learn everything you need to know to master asynchronous programming using async await in C# and .NET
No Dogma Podcast, Hosted by Bryan Hogan
NDC London 2026
Correcting Common Async Await Mistakes in .NET 10
Async/await is great but there are two subtle problems that can easily creep into code:
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.
tl;dr A non-awaited Task doesn't rethrow exceptions and AsyncAwaitBestPractices.SafeFireAndForget ensures it will

(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 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.
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!
MoveNextNow 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:
await keyword (Prefered)
await DoSomethingAsync().GetAwaiter().GetResult()
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.
.Result or .Wait()?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 the Task has completed.
.Result or .Wait() rethrow your exception as a System.AggregateException, which makes it difficult to find the actual exception.
SafeFireAndForgetAn extension method to safely fire-and-forget a Task.
SafeFireAndForget allows a Task to safely run on a different thread while the calling thread does not wait for its completion.
public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, System.Action<System.Exception>? onException = null, bool continueOnCapturedContext = false)
public static async void SafeFireAndForget(this System.Threading.Tasks.ValueTask task, System.Action<System.Exception>? onException = null, bool continueOnCapturedContext = false)
.NET 8.0 Introduces ConfigureAwaitOptions that allow users to customize the behavior when awaiting:
ConfigureAwaitOptions.None
ConfigureAwaitOptions.SuppressThrowing
ConfigureAwaitOptions.ContinueOnCapturedContext
ConfigureAwaitOptions.ForceYielding
For more information, check out Stephen Cleary's blog post, "ConfigureAwait in .NET 8".
public static void SafeFireAndForget(this System.Threading.Tasks.Task task, ConfigureAwaitOptions configureAwaitOptions, Action<Exception>? onException = null)
void HandleButtonTapped(object sender, EventArgs e)
{
// Allows the async Task method to safely run on a different thread while the calling thread continues, not awaiting its completion
// onException: If an Exception is thrown, print it to the Console
ExampleAsyncMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex));
// HandleButtonTapped continues execution here while `ExampleAsyncMethod()` is running on a different thread
// ...
}
async Task ExampleAsyncMethod()
{
await Task.Delay(1000);
}
Note:
ConfigureAwaitOptions.SuppressThrowingwill always supress exceptions from being rethrown. This means thatonExceptionwill never execute whenConfigureAwaitOptions.SuppressThrowingis set.
If you're new to ValueTask, check out this great write-up, Understanding the Whys, Whats, and Whens of ValueTask .
void HandleButtonTapped(object sender, EventArgs e)
{
// Allows the async ValueTask method to safely run on a different thread while the calling thread continues, not awaiting its completion
// onException: If an Exception is thrown, print it to the Console
ExampleValueTaskMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex));
// HandleButtonTapped continues execution here while `ExampleAsyncMethod()` is running on a different thread
// ...
}
async ValueTask ExampleValueTaskMethod()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
}
void InitializeSafeFireAndForget()
{
// Initialize SafeFireAndForget
// Only use `shouldAlwaysRethrowException: true` when you want `.SafeFireAndForget()` to always rethrow every exception. This is not recommended, because there is no way to catch an Exception rethrown by `SafeFireAndForget()`; `shouldAlwaysRethrowException: true` should **not** be used in Production/Release builds.
SafeFireAndForgetExtensions.Initialize(shouldAlwaysRethrowException: false);
// SafeFireAndForget will print every exception to the Console
SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => Console.WriteLine(ex));
}
void UninitializeSafeFireAndForget()
{
// Remove default exception handling
SafeFireAndForgetExtensions.RemoveDefaultExceptionHandling();
}
void HandleButtonTapped(object sender, EventArgs e)
{
// Allows the async Task method to safely run on a different thread while not awaiting its completion
// onException: If a WebException is thrown, print its StatusCode to the Console. **Note**: If a non-WebException is thrown, it will not be handled by `onException`
// Because we set `SetDefaultExceptionHandling` in `void InitializeSafeFireAndForget()`, the entire exception will also be printed to the Console
ExampleAsyncMethod().SafeFireAndForget<WebException>(onException: ex =>
{
if(ex.Response is HttpWebResponse webResponse)
Console.WriteLine($"Task Exception\n Status Code: {webResponse.StatusCode}");
});
ExampleValueTaskMethod().SafeFireAndForget<WebException>(onException: ex =>
{
if(ex.Response is HttpWebResponse webResponse)
Console.WriteLine($"ValueTask Error\n Status Code: {webResponse.StatusCode}");
});
// HandleButtonTapped continues execution here while `ExampleAsyncMethod()` and `ExampleValueTaskMethod()` run in the background
}
async Task ExampleAsyncMethod()
{
await Task.Delay(1000);
throw new WebException();
}
async ValueTask ExampleValueTaskMethod()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
throw new WebException();
}
Note:
ConfigureAwaitOptions.SuppressThrowingwill always supress exceptions from being rethrown. This means thatonExceptionwill never execute whenConfigureAwaitOptions.SuppressThrowingis set.
WeakEventManagerAn event implementation that enables the garbage collector to collect an object without needing to unsubscribe event handlers.
Inspired by Xamarin.Forms.WeakEventManager.
EventHandlerreadonly WeakEventManager _canExecuteChangedEventManager = new WeakEventManager();
public event EventHandler CanExecuteChanged
{
add => _canExecuteChangedEventManager.AddEventHandler(value);
remove => _canExecuteChangedEventManager.RemoveEventHandler(value);
}
void OnCanExecuteChanged() => _canExecuteChangedEventManager.RaiseEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));
Delegatereadonly WeakEventManager _propertyChangedEventManager = new WeakEventManager();
public event PropertyChangedEventHandler PropertyChanged
{
add => _propertyChangedEventManager.AddEventHandler(value);
remove => _propertyChangedEventManager.RemoveEventHandler(value);
}
void OnPropertyChanged([CallerMemberName]string propertyName = "") => _propertyChangedEventManager.RaiseEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged));
Actionreadonly WeakEventManager _weakActionEventManager = new WeakEventManager();
public event Action ActionEvent
{
add => _weakActionEventManager.AddEventHandler(value);
remove => _weakActionEventManager.RemoveEventHandler(value);
}
void OnActionEvent(string message) => _weakActionEventManager.RaiseEvent(message, nameof(ActionEvent));
WeakEventManager<T>An event implementation that enables the garbage collector to collect an object without needing to unsubscribe event handlers.
Inspired by Xamarin.Forms.WeakEventManager.
EventHandler<T>readonly WeakEventManager<string> _errorOcurredEventManager = new WeakEventManager<string>();
public event EventHandler<string> ErrorOcurred
{
add => _errorOcurredEventManager.AddEventHandler(value);
remove => _errorOcurredEventManager.RemoveEventHandler(value);
}
void OnErrorOcurred(string message) => _errorOcurredEventManager.RaiseEvent(this, message, nameof(ErrorOcurred));
Action<T>readonly WeakEventManager<string> _weakActionEventManager = new WeakEventManager<string>();
public event Action<string> ActionEvent
{
add => _weakActionEventManager.AddEventHandler(value);
remove => _weakActionEventManager.RemoveEventHandler(value);
}
void OnActionEvent(string message) => _weakActionEventManager.RaiseEvent(message, nameof(ActionEvent));
AsyncCommandAllows for Task to safely be used asynchronously with ICommand:
AsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute, TCanExecute>IAsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute>AsyncCommand<T> : IAsyncCommand<T>IAsyncCommand<T> : ICommandAsyncCommand : IAsyncCommandIAsyncCommand : ICommandpublic AsyncCommand(Func<TExecute, Task> execute,
Func<TCanExecute, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncCommand(Func<T, Task> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncCommand(Func<Task> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public class ExampleClass
{
bool _isBusy;
public ExampleClass()
{
ExampleAsyncCommand = new AsyncCommand(ExampleAsyncMethod);
ExampleAsyncIntCommand = new AsyncCommand<int>(ExampleAsyncMethodWithIntParameter);
ExampleAsyncIntCommandWithCanExecute = new AsyncCommand<int, int>(ExampleAsyncMethodWithIntParameter, CanExecuteInt);
ExampleAsyncExceptionCommand = new AsyncCommand(ExampleAsyncMethodWithException, onException: ex => Console.WriteLine(ex.ToString()));
ExampleAsyncCommandWithCanExecuteChanged = new AsyncCommand(ExampleAsyncMethod, _ => !IsBusy);
ExampleAsyncCommandReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: true);
}
public IAsyncCommand ExampleAsyncCommand { get; }
public IAsyncCommand<int> ExampleAsyncIntCommand { get; }
public IAsyncCommand<int, int> ExampleAsyncIntCommandWithCanExecute { get; }
public IAsyncCommand ExampleAsyncExceptionCommand { get; }
public IAsyncCommand ExampleAsyncCommandWithCanExecuteChanged { get; }
public IAsyncCommand ExampleAsyncCommandReturningToTheCallingThread { get; }
public bool IsBusy
{
get => _isBusy;
set
{
if (_isBusy != value)
{
_isBusy = value;
ExampleAsyncCommandWithCanExecuteChanged.RaiseCanExecuteChanged();
}
}
}
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();
}
bool CanExecuteInt(int count)
{
if(count > 2)
return true;
return false;
}
void ExecuteCommands()
{
_isBusy = true;
try
{
ExampleAsyncCommand.Execute(null);
ExampleAsyncIntCommand.Execute(1000);
ExampleAsyncExceptionCommand.Execute(null);
ExampleAsyncCommandReturningToTheCallingThread.Execute(null);
if(ExampleAsyncCommandWithCanExecuteChanged.CanExecute(null))
ExampleAsyncCommandWithCanExecuteChanged.Execute(null);
if(ExampleAsyncIntCommandWithCanExecute.CanExecute(1))
ExampleAsyncIntCommandWithCanExecute.Execute(1);
}
finally
{
_isBusy = false;
}
}
}
AsyncValueCommandAllows for ValueTask to safely be used asynchronously with ICommand.
If you're new to ValueTask, check out this great write-up, Understanding the Whys, Whats, and Whens of ValueTask .
AsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute, TCanExecute>IAsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute>AsyncValueCommand<T> : IAsyncValueCommand<T>IAsyncValueCommand<T> : ICommandAsyncValueCommand : IAsyncValueCommandIAsyncValueCommand : ICommandpublic AsyncValueCommand(Func<TExecute, ValueTask> execute,
Func<TCanExecute, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncValueCommand(Func<T, ValueTask> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncValueCommand(Func<ValueTask> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public class ExampleClass
{
bool _isBusy;
public ExampleClass()
{
ExampleValueTaskCommand = new AsyncValueCommand(ExampleValueTaskMethod);
ExampleValueTaskIntCommand = new AsyncValueCommand<int>(ExampleValueTaskMethodWithIntParameter);
ExampleValueTaskIntCommandWithCanExecute = new AsyncValueCommand<int, int>(ExampleValueTaskMethodWithIntParameter, CanExecuteInt);
ExampleValueTaskExceptionCommand = new AsyncValueCommand(ExampleValueTaskMethodWithException, onException: ex => Debug.WriteLine(ex.ToString()));
ExampleValueTaskCommandWithCanExecuteChanged = new AsyncValueCommand(ExampleValueTaskMethod, _ => !IsBusy);
ExampleValueTaskCommandReturningToTheCallingThread = new AsyncValueCommand(ExampleValueTaskMethod, continueOnCapturedContext: true);
}
public IAsyncValueCommand ExampleValueTaskCommand { get; }
public IAsyncValueCommand<int> ExampleValueTaskIntCommand { get; }
public IAsyncCommand<int, int> ExampleValueTaskIntCommandWithCanExecute { get; }
public IAsyncValueCommand ExampleValueTaskExceptionCommand { get; }
public IAsyncValueCommand ExampleValueTaskCommandWithCanExecuteChanged { get; }
public IAsyncValueCommand ExampleValueTaskCommandReturningToTheCallingThread { get; }
public bool IsBusy
{
get => _isBusy;
set
{
if (_isBusy != value)
{
_isBusy = value;
ExampleValueTaskCommandWithCanExecuteChanged.RaiseCanExecuteChanged();
}
}
}
async ValueTask ExampleValueTaskMethod()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
}
async ValueTask ExampleValueTaskMethodWithIntParameter(int parameter)
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(parameter);
}
async ValueTask ExampleValueTaskMethodWithException()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
throw new Exception();
}
bool CanExecuteInt(int count)
{
if(count > 2)
return true;
return false;
}
void ExecuteCommands()
{
_isBusy = true;
try
{
ExampleValueTaskCommand.Execute(null);
ExampleValueTaskIntCommand.Execute(1000);
ExampleValueTaskExceptionCommand.Execute(null);
ExampleValueTaskCommandReturningToTheCallingThread.Execute(null);
if (ExampleValueTaskCommandWithCanExecuteChanged.CanExecute(null))
ExampleValueTaskCommandWithCanExecuteChanged.Execute(null);
if(ExampleValueTaskIntCommandWithCanExecute.CanExecute(2))
ExampleValueTaskIntCommandWithCanExecute.Execute(2);
}
finally
{
_isBusy = false;
}
}
}
140 followers · starred Nov 2025
34 followers · starred Jan 2020
477 followers · starred Nov 2022
162 followers · starred Oct 2023
Extensions for System.Threading.Tasks.Task and System.Threading.Tasks.ValueTask
See the codeExtensions for System.Threading.Tasks.Task.
Inspired by John Thiriet's blog posts:
Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices/
SafeFireAndForget
Task or a ValueTaskTask will rethrow an Exception if an Exception is caught in IAsyncStateMachine.MoveNext()WeakEventManager
AsyncCommand, AsyncCommand<T>, AsyncValueCommand, AsyncValueCommand<T>Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices.MVVM/
Allows for Task to safely be used asynchronously with ICommand:
IAsyncCommand : ICommandAsyncCommand : IAsyncCommandIAsyncCommand<T> : ICommandAsyncCommand<T> : IAsyncCommand<T>IAsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute>AsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute, TCanExecute>Allows for ValueTask to safely be used asynchronously with ICommand:
IAsyncValueCommand : ICommandAsyncValueCommand : IAsyncValueCommandIAsyncValueCommand<T> : ICommandAsyncValueCommand<T> : IAsyncValueCommand<T>IAsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute>AsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute, TCanExecute>The author of this library teaches a hands-on async/await and parallel programming workshop for C# teams (taught at NDC London and NDC Oslo in 2026) and offers async audits of production code. Details and pricing: codetraveler.io/workshops and codetraveler.io/dotnet-consulting.
Join me in these DomeTrain courses where we'll learn everything you need to know to master asynchronous programming using async await in C# and .NET
No Dogma Podcast, Hosted by Bryan Hogan
NDC London 2026
Correcting Common Async Await Mistakes in .NET 10
Async/await is great but there are two subtle problems that can easily creep into code:
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.
tl;dr A non-awaited Task doesn't rethrow exceptions and AsyncAwaitBestPractices.SafeFireAndForget ensures it will

(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 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.
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!
MoveNextNow 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:
await keyword (Prefered)
await DoSomethingAsync().GetAwaiter().GetResult()
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.
.Result or .Wait()?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 the Task has completed.
.Result or .Wait() rethrow your exception as a System.AggregateException, which makes it difficult to find the actual exception.
SafeFireAndForgetAn extension method to safely fire-and-forget a Task.
SafeFireAndForget allows a Task to safely run on a different thread while the calling thread does not wait for its completion.
public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, System.Action<System.Exception>? onException = null, bool continueOnCapturedContext = false)
public static async void SafeFireAndForget(this System.Threading.Tasks.ValueTask task, System.Action<System.Exception>? onException = null, bool continueOnCapturedContext = false)
.NET 8.0 Introduces ConfigureAwaitOptions that allow users to customize the behavior when awaiting:
ConfigureAwaitOptions.None
ConfigureAwaitOptions.SuppressThrowing
ConfigureAwaitOptions.ContinueOnCapturedContext
ConfigureAwaitOptions.ForceYielding
For more information, check out Stephen Cleary's blog post, "ConfigureAwait in .NET 8".
public static void SafeFireAndForget(this System.Threading.Tasks.Task task, ConfigureAwaitOptions configureAwaitOptions, Action<Exception>? onException = null)
void HandleButtonTapped(object sender, EventArgs e)
{
// Allows the async Task method to safely run on a different thread while the calling thread continues, not awaiting its completion
// onException: If an Exception is thrown, print it to the Console
ExampleAsyncMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex));
// HandleButtonTapped continues execution here while `ExampleAsyncMethod()` is running on a different thread
// ...
}
async Task ExampleAsyncMethod()
{
await Task.Delay(1000);
}
Note:
ConfigureAwaitOptions.SuppressThrowingwill always supress exceptions from being rethrown. This means thatonExceptionwill never execute whenConfigureAwaitOptions.SuppressThrowingis set.
If you're new to ValueTask, check out this great write-up, Understanding the Whys, Whats, and Whens of ValueTask .
void HandleButtonTapped(object sender, EventArgs e)
{
// Allows the async ValueTask method to safely run on a different thread while the calling thread continues, not awaiting its completion
// onException: If an Exception is thrown, print it to the Console
ExampleValueTaskMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex));
// HandleButtonTapped continues execution here while `ExampleAsyncMethod()` is running on a different thread
// ...
}
async ValueTask ExampleValueTaskMethod()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
}
void InitializeSafeFireAndForget()
{
// Initialize SafeFireAndForget
// Only use `shouldAlwaysRethrowException: true` when you want `.SafeFireAndForget()` to always rethrow every exception. This is not recommended, because there is no way to catch an Exception rethrown by `SafeFireAndForget()`; `shouldAlwaysRethrowException: true` should **not** be used in Production/Release builds.
SafeFireAndForgetExtensions.Initialize(shouldAlwaysRethrowException: false);
// SafeFireAndForget will print every exception to the Console
SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => Console.WriteLine(ex));
}
void UninitializeSafeFireAndForget()
{
// Remove default exception handling
SafeFireAndForgetExtensions.RemoveDefaultExceptionHandling();
}
void HandleButtonTapped(object sender, EventArgs e)
{
// Allows the async Task method to safely run on a different thread while not awaiting its completion
// onException: If a WebException is thrown, print its StatusCode to the Console. **Note**: If a non-WebException is thrown, it will not be handled by `onException`
// Because we set `SetDefaultExceptionHandling` in `void InitializeSafeFireAndForget()`, the entire exception will also be printed to the Console
ExampleAsyncMethod().SafeFireAndForget<WebException>(onException: ex =>
{
if(ex.Response is HttpWebResponse webResponse)
Console.WriteLine($"Task Exception\n Status Code: {webResponse.StatusCode}");
});
ExampleValueTaskMethod().SafeFireAndForget<WebException>(onException: ex =>
{
if(ex.Response is HttpWebResponse webResponse)
Console.WriteLine($"ValueTask Error\n Status Code: {webResponse.StatusCode}");
});
// HandleButtonTapped continues execution here while `ExampleAsyncMethod()` and `ExampleValueTaskMethod()` run in the background
}
async Task ExampleAsyncMethod()
{
await Task.Delay(1000);
throw new WebException();
}
async ValueTask ExampleValueTaskMethod()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
throw new WebException();
}
Note:
ConfigureAwaitOptions.SuppressThrowingwill always supress exceptions from being rethrown. This means thatonExceptionwill never execute whenConfigureAwaitOptions.SuppressThrowingis set.
WeakEventManagerAn event implementation that enables the garbage collector to collect an object without needing to unsubscribe event handlers.
Inspired by Xamarin.Forms.WeakEventManager.
EventHandlerreadonly WeakEventManager _canExecuteChangedEventManager = new WeakEventManager();
public event EventHandler CanExecuteChanged
{
add => _canExecuteChangedEventManager.AddEventHandler(value);
remove => _canExecuteChangedEventManager.RemoveEventHandler(value);
}
void OnCanExecuteChanged() => _canExecuteChangedEventManager.RaiseEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));
Delegatereadonly WeakEventManager _propertyChangedEventManager = new WeakEventManager();
public event PropertyChangedEventHandler PropertyChanged
{
add => _propertyChangedEventManager.AddEventHandler(value);
remove => _propertyChangedEventManager.RemoveEventHandler(value);
}
void OnPropertyChanged([CallerMemberName]string propertyName = "") => _propertyChangedEventManager.RaiseEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged));
Actionreadonly WeakEventManager _weakActionEventManager = new WeakEventManager();
public event Action ActionEvent
{
add => _weakActionEventManager.AddEventHandler(value);
remove => _weakActionEventManager.RemoveEventHandler(value);
}
void OnActionEvent(string message) => _weakActionEventManager.RaiseEvent(message, nameof(ActionEvent));
WeakEventManager<T>An event implementation that enables the garbage collector to collect an object without needing to unsubscribe event handlers.
Inspired by Xamarin.Forms.WeakEventManager.
EventHandler<T>readonly WeakEventManager<string> _errorOcurredEventManager = new WeakEventManager<string>();
public event EventHandler<string> ErrorOcurred
{
add => _errorOcurredEventManager.AddEventHandler(value);
remove => _errorOcurredEventManager.RemoveEventHandler(value);
}
void OnErrorOcurred(string message) => _errorOcurredEventManager.RaiseEvent(this, message, nameof(ErrorOcurred));
Action<T>readonly WeakEventManager<string> _weakActionEventManager = new WeakEventManager<string>();
public event Action<string> ActionEvent
{
add => _weakActionEventManager.AddEventHandler(value);
remove => _weakActionEventManager.RemoveEventHandler(value);
}
void OnActionEvent(string message) => _weakActionEventManager.RaiseEvent(message, nameof(ActionEvent));
AsyncCommandAllows for Task to safely be used asynchronously with ICommand:
AsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute, TCanExecute>IAsyncCommand<TExecute, TCanExecute> : IAsyncCommand<TExecute>AsyncCommand<T> : IAsyncCommand<T>IAsyncCommand<T> : ICommandAsyncCommand : IAsyncCommandIAsyncCommand : ICommandpublic AsyncCommand(Func<TExecute, Task> execute,
Func<TCanExecute, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncCommand(Func<T, Task> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncCommand(Func<Task> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public class ExampleClass
{
bool _isBusy;
public ExampleClass()
{
ExampleAsyncCommand = new AsyncCommand(ExampleAsyncMethod);
ExampleAsyncIntCommand = new AsyncCommand<int>(ExampleAsyncMethodWithIntParameter);
ExampleAsyncIntCommandWithCanExecute = new AsyncCommand<int, int>(ExampleAsyncMethodWithIntParameter, CanExecuteInt);
ExampleAsyncExceptionCommand = new AsyncCommand(ExampleAsyncMethodWithException, onException: ex => Console.WriteLine(ex.ToString()));
ExampleAsyncCommandWithCanExecuteChanged = new AsyncCommand(ExampleAsyncMethod, _ => !IsBusy);
ExampleAsyncCommandReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: true);
}
public IAsyncCommand ExampleAsyncCommand { get; }
public IAsyncCommand<int> ExampleAsyncIntCommand { get; }
public IAsyncCommand<int, int> ExampleAsyncIntCommandWithCanExecute { get; }
public IAsyncCommand ExampleAsyncExceptionCommand { get; }
public IAsyncCommand ExampleAsyncCommandWithCanExecuteChanged { get; }
public IAsyncCommand ExampleAsyncCommandReturningToTheCallingThread { get; }
public bool IsBusy
{
get => _isBusy;
set
{
if (_isBusy != value)
{
_isBusy = value;
ExampleAsyncCommandWithCanExecuteChanged.RaiseCanExecuteChanged();
}
}
}
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();
}
bool CanExecuteInt(int count)
{
if(count > 2)
return true;
return false;
}
void ExecuteCommands()
{
_isBusy = true;
try
{
ExampleAsyncCommand.Execute(null);
ExampleAsyncIntCommand.Execute(1000);
ExampleAsyncExceptionCommand.Execute(null);
ExampleAsyncCommandReturningToTheCallingThread.Execute(null);
if(ExampleAsyncCommandWithCanExecuteChanged.CanExecute(null))
ExampleAsyncCommandWithCanExecuteChanged.Execute(null);
if(ExampleAsyncIntCommandWithCanExecute.CanExecute(1))
ExampleAsyncIntCommandWithCanExecute.Execute(1);
}
finally
{
_isBusy = false;
}
}
}
AsyncValueCommandAllows for ValueTask to safely be used asynchronously with ICommand.
If you're new to ValueTask, check out this great write-up, Understanding the Whys, Whats, and Whens of ValueTask .
AsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute, TCanExecute>IAsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute>AsyncValueCommand<T> : IAsyncValueCommand<T>IAsyncValueCommand<T> : ICommandAsyncValueCommand : IAsyncValueCommandIAsyncValueCommand : ICommandpublic AsyncValueCommand(Func<TExecute, ValueTask> execute,
Func<TCanExecute, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncValueCommand(Func<T, ValueTask> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public AsyncValueCommand(Func<ValueTask> execute,
Func<object?, bool>? canExecute = null,
Action<Exception>? onException = null,
bool continueOnCapturedContext = false)
public class ExampleClass
{
bool _isBusy;
public ExampleClass()
{
ExampleValueTaskCommand = new AsyncValueCommand(ExampleValueTaskMethod);
ExampleValueTaskIntCommand = new AsyncValueCommand<int>(ExampleValueTaskMethodWithIntParameter);
ExampleValueTaskIntCommandWithCanExecute = new AsyncValueCommand<int, int>(ExampleValueTaskMethodWithIntParameter, CanExecuteInt);
ExampleValueTaskExceptionCommand = new AsyncValueCommand(ExampleValueTaskMethodWithException, onException: ex => Debug.WriteLine(ex.ToString()));
ExampleValueTaskCommandWithCanExecuteChanged = new AsyncValueCommand(ExampleValueTaskMethod, _ => !IsBusy);
ExampleValueTaskCommandReturningToTheCallingThread = new AsyncValueCommand(ExampleValueTaskMethod, continueOnCapturedContext: true);
}
public IAsyncValueCommand ExampleValueTaskCommand { get; }
public IAsyncValueCommand<int> ExampleValueTaskIntCommand { get; }
public IAsyncCommand<int, int> ExampleValueTaskIntCommandWithCanExecute { get; }
public IAsyncValueCommand ExampleValueTaskExceptionCommand { get; }
public IAsyncValueCommand ExampleValueTaskCommandWithCanExecuteChanged { get; }
public IAsyncValueCommand ExampleValueTaskCommandReturningToTheCallingThread { get; }
public bool IsBusy
{
get => _isBusy;
set
{
if (_isBusy != value)
{
_isBusy = value;
ExampleValueTaskCommandWithCanExecuteChanged.RaiseCanExecuteChanged();
}
}
}
async ValueTask ExampleValueTaskMethod()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
}
async ValueTask ExampleValueTaskMethodWithIntParameter(int parameter)
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(parameter);
}
async ValueTask ExampleValueTaskMethodWithException()
{
var random = new Random();
if (random.Next(10) > 9)
await Task.Delay(1000);
throw new Exception();
}
bool CanExecuteInt(int count)
{
if(count > 2)
return true;
return false;
}
void ExecuteCommands()
{
_isBusy = true;
try
{
ExampleValueTaskCommand.Execute(null);
ExampleValueTaskIntCommand.Execute(1000);
ExampleValueTaskExceptionCommand.Execute(null);
ExampleValueTaskCommandReturningToTheCallingThread.Execute(null);
if (ExampleValueTaskCommandWithCanExecuteChanged.CanExecute(null))
ExampleValueTaskCommandWithCanExecuteChanged.Execute(null);
if(ExampleValueTaskIntCommandWithCanExecute.CanExecute(2))
ExampleValueTaskIntCommandWithCanExecute.Execute(2);
}
finally
{
_isBusy = false;
}
}
}
140 followers · starred Nov 2025
34 followers · starred Jan 2020
477 followers · starred Nov 2022
162 followers · starred Oct 2023