AsyncAwaitBestPractices

repository·main·Indexed 23 days ago

https://github.com/thecodetraveler/asyncawaitbestpractices

A library providing extensions for System.Threading.Tasks.Task and MVVM command implementations to address common asynchronous programming pitfalls. It features SafeFireAndForget to prevent silent exceptions in fire-and-forget tasks, WeakEventManager to avoid memory leaks, and specialized async command implementations (AsyncCommand and AsyncValueCommand) for using Task and ValueTask with ICommand in MVVM patterns. Compatible with .NET Standard 1.0.

Tokens
8.4K
Snippets
14
Records
23
Agent score
84%

What's inside AsyncAwaitBestPractices

  1. Understand why SafeFireAndForget is needed

    main

    When using async methods, the compiler transforms them into an IAsyncStateMachine class. The MoveNext() method, which executes the method's logic, is automatically wrapped in a try/catch block by the compiler.

    Because of this, exceptions thrown inside an async method are caught internally by the state machine. If the task is not awaited, the exception is caught but never surfaced to the developer, making debugging extremely difficult. SafeFireAndForget solves this by ensuring these exceptions are rethrown.

  2. Configure global exception handling for `SafeFireAndForget`

    main

    You can initialize the SafeFireAndForget extension methods with global settings for exception handling.

    Initialization

    Use SafeFireAndForgetExtensions.Initialize to set whether exceptions should always be rethrown. Warning: Setting shouldAlwaysRethrowException: true is not recommended for Production/Release builds because there is no way to catch exceptions rethrown by SafeFireAndForget().

    Use SafeFireAndForgetExtensions.SetDefaultExceptionHandling to define a global handler for all fire-and-forget tasks.

    Example

    void InitializeSafeFireAndForget()
    {
        // Initialize SafeFireAndForget
        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 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();
    }
  3. Implement events using WeakEventManager

    main

    To implement a weak event pattern, use WeakEventManager or WeakEventManager<T> to manage the underlying event storage. You then expose a standard C# event that uses the manager's AddEventHandler and RemoveEventHandler methods.

    Using EventHandler or Action with WeakEventManager

    readonly 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));

    Using EventHandler<T> or Action<T> with WeakEventManager<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));
    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));
  4. Best practices for rethrowing exceptions in async methods

    main

    To ensure exceptions thrown in an async method are visible, use one of the following methods:

    1. Use the await keyword (Preferred): This allows the Task to run asynchronously on a different thread without locking the current thread.
      • Example: await DoSomethingAsync()
    2. Use .GetAwaiter().GetResult(): This is an alternative, but less preferred than await.
      • Example: DoSomethingAsync().GetAwaiter().GetResult()

    What to avoid

    Never use .Result or .Wait():

    • They lock up the current thread (which can freeze the UI thread).
    • They rethrow exceptions as System.AggregateException, which obscures the actual error.
  5. Use SafeFireAndForget with specific exception types

    main

    You can use the generic version of SafeFireAndForget<TException> to provide an exception handler that only triggers for a specific exception type. If a different exception type is thrown, it will fall back to the global handler set via SetDefaultExceptionHandling (if any).

    void HandleButtonTapped(object sender, EventArgs e)
    {
        // Only handles WebException; other exceptions use the global handler
        ExampleAsyncMethod().SafeFireAndForget<WebException>(onException: ex =>
        {
            if(ex.Response is HttpWebResponse webResponse)
                Console.WriteLine($"Task Exception\n Status Code: {webResponse.StatusCode}");
        });
    }
    
    async Task ExampleAsyncMethod()
    {
        await Task.Delay(1000);
        throw new WebException();
    }
  6. Configure `SafeFireAndForget` with `ConfigureAwaitOptions` (.NET 8+)

    main

    In .NET 8.0 and higher, you can use ConfigureAwaitOptions to customize how the background task behaves when it completes.

    Available Options

    • ConfigureAwaitOptions.None: No options specified.
    • ConfigureAwaitOptions.SuppressThrowing: Avoids throwing an exception at the completion of awaiting a Task that ends in the Faulted or Canceled state. Note: When this is set, onException will never execute because exceptions are suppressed from being rethrown.
    • ConfigureAwaitOptions.ContinueOnCapturedContext: Attempts to marshal the continuation back to the original SynchronizationContext or TaskScheduler present on the originating thread.
    • ConfigureAwaitOptions.ForceYielding: Forces an await on an already completed Task to behave as if the Task wasn't yet completed, forcing the current asynchronous method to yield execution.

    Signature

    public static void SafeFireAndForget(this System.Threading.Tasks.Task task, ConfigureAwaitOptions configureAwaitOptions, Action<Exception>? onException = null)