Giter Club home page Giter Club logo

asyncawaitbestpractices's Introduction

AsyncAwaitBestPractices

Build Status

Extensions for System.Threading.Tasks.Task.

Inspired by John Thiriet's blog posts:

AsyncAwaitBestPractices

NuGet

Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices/

  • SafeFireAndForget
    • An extension method to safely fire-and-forget a Task or a ValueTask
    • Ensures the Task will rethrow an Exception if an Exception is caught in IAsyncStateMachine.MoveNext()
  • WeakEventManager
    • Avoids memory leaks when events are not unsubscribed
    • Used by AsyncCommand, AsyncCommand<T>, AsyncValueCommand, AsyncValueCommand<T>
  • Usage instructions

AsyncAwaitBestPractices.MVVM

NuGet

  • Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices.MVVM/

  • Allows for Task to safely be used asynchronously with ICommand:

    • IAsyncCommand : ICommand
    • AsyncCommand : IAsyncCommand
    • IAsyncCommand<T> : ICommand
    • AsyncCommand<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 : ICommand
    • AsyncValueCommand : IAsyncValueCommand
    • IAsyncValueCommand<T> : ICommand
    • AsyncValueCommand<T> : IAsyncValueCommand<T>
    • IAsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute>
    • AsyncValueCommand<TExecute, TCanExecute> : IAsyncValueCommand<TExecute, TCanExecute>
  • Usage instructions

Setup

AsyncAwaitBestPractices

AsyncAwaitBestPractices.MVVM

Why Do I Need This?

Podcasts

No Dogma Podcast, Hosted by Bryan Hogan

Video

NDC London 2024

Correcting Common Async Await Mistakes in .NET 8

Explaination

Async/await is great but there are two subtle problems that can easily creep into code:

  1. Creating race conditions/concurrent execution (where you code things in the right order but the code executes in a different order than you expect)
  2. 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.

tl;dr A non-awaited Task doesn't rethrow exceptions and AsyncAwaitBestPractices.SafeFireAndForget ensures it will

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 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:

  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

SafeFireAndForget

An 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)

On .NET 8.0 (and higher)

.NET 8.0 Introduces ConfigureAwaitOptions that allow users to customize the behavior when awaiting:

  • 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
  • ConfigureAwaitOptions.ContinueOnCapturedContext
    • Attempts to marshal the continuation back to the original SynchronizationContext or TaskScheduler present on the originating thread at the time of the await
  • ConfigureAwaitOptions.ForceYielding
    • Forces an await on an already completed Task to behave as if the Task wasn't yet completed, such that the current asynchronous method will be forced to yield its execution

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)

Basic Usage - Task

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.SuppressThrowing will always supress exceptions from being rethrown. This means that onException will never execute when ConfigureAwaitOptions.SuppressThrowing is set.

Basic Usage - ValueTask

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

Advanced Usage

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.SuppressThrowing will always supress exceptions from being rethrown. This means that onException will never execute when ConfigureAwaitOptions.SuppressThrowing is set.

WeakEventManager

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

Inspired by Xamarin.Forms.WeakEventManager.

Using EventHandler

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 Delegate

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

Using Action

readonly 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.

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

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

AsyncAwaitBestPractices.MVVM

AsyncCommand

Allows 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> : ICommand
  • AsyncCommand : IAsyncCommand
  • IAsyncCommand : ICommand
public 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;
        }
    }
}

AsyncValueCommand

Allows 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> : ICommand
  • AsyncValueCommand : IAsyncValueCommand
  • IAsyncValueCommand : ICommand
public 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;
        }
    }
}

Learn More

asyncawaitbestpractices's People

Contributors

andrekiba avatar andrewchungxam avatar azureadvocatebit avatar billwagner avatar brminnick avatar ccrcmcpe avatar dependabot[bot] avatar khalidabuhakmeh avatar llaughlin avatar pasisavolainen avatar slang25 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

asyncawaitbestpractices's Issues

Possible improvement for explicit ICommand.Execute() implementation in AsyncCommand?

While looking at your awesome example implementation of IAsyncCommand, I noticed that the explicit implementation of ICommand.Execute() over at

void ICommand.Execute(object parameter) => _execute().SafeFireAndForget(_onException, in _continueOnCapturedContext);

points to _execute() instead of ExecuteAsync() (2 lines above) - out of curiosity: why's that?

Because if the latter would be used, an implementer would have a single point of customization, e. g. for extension of ExecuteAsync() by setting an IsExecuting property via try ... finally (checked inside CanExecute(), of course) to provide for efficient prevention of consecutive calls prior to actually finishing the first execution.

WinRT: Windows 8.1 SafeFireAndForget issue "No overload for method"

Hello,

I noticed an error on my WinRT project (Windows 8.1) using your package.
I can't use the SafeFireAndForget extension method because I have this message :
No overload for method 'SafeFireAndForget' takes 1 arguments

image

Do you have more information about this problem?

Thanks

Nuget package version : 4.1.0

Microsoft Visual Studio Enterprise 2015
Version 14.0.25431.01 Update 3
Microsoft .NET Framework
Version 4.8.03752

Async / Await != Multithread

Just saw your talk.
the first explanation of Async and await behavior is a bit wrong / misleading.

see:
https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/task-asynchronous-programming-model#BKMK_Threads

Simplified:
Async and await cut long work into pieces and each piece is run on the synchronization context (kind of the windows message loop).
This can be multi threaded, but doesn’t have to.

The idea is, at some point down the chain some long running operation is handled by the OS, for example IO or Network. When the OS is finished the result will be available via a message on the message loop(dependening on the synchronization context). At this point the continuation can be run.

If this seems super strange, build a barebones C++ win32 app, in that case you have to actually build the message loop.
If you debug that app you will notice a lot of the time your app is actually doing, …. Nothing?! The app is Idle.

idle is the next concept to understand. Every process has an allotted time. The time from callback into the synchronization context until returning to the OS measured against the allotted time is the percentage your app is taking from the cpu. Depending on the amount of cores, this will be split accordingly. So 4 cores, without multithreading max 25% cpu usage if your app consumes 100% of the allotted time.

Important to understand is that, redrawing the UI should not take 100% of this time, so the remaining time is spend to finish Async methods. Those as well should be short, otherwise what you described with a hanging UI will happen regardless of using or not using Async / await.

I can recommend the long blog post of stehphen toub.

https://devblogs.microsoft.com/dotnet/how-async-await-really-works/

Best

ArgumentException with Dynamic Handlers

This issue is for .NET Core 3.1 but I believe it would also occur in .NET Framework 4.8 (and probably lower versions).

This issue is for AsyncAwaitBestPractices V4.2.0.

I believe EventManagerService.HandleEvent is throwing exceptions trying to call Invoke on a dynamic delegate that requires DynamicInvoke to be called.

I discovered this in unit tests in my project. I am using FluentAssertions and checking for property changed events to be fired. See the example below:

var vm = new ViewModel();
using var subject = vm.Monitor();
vm.SomeMethod();
subject.Should().RaisePropertyChangeFor(x => x.Value);

I you are not familiar with FluentAssertions, calling vm.Monitor() dynamically creates handlers for all events on the vm object so you can assert behavior of the events.

Here is the stack trace from when the exception occurs.

  Message: 
    System.ArgumentException : MethodInfo must be a runtime MethodInfo object. (Parameter 'this')
  Stack Trace: 
    RTDynamicMethod.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
    EventManagerService.HandleEvent(String& eventName, Object& sender, Object& eventArgs, Dictionary`2& eventHandlers)
    WeakEventManager.RaiseEvent(Object& sender, Object& eventArgs, String& eventName)
    WeakEventManager.RaiseEvent(Object sender, Object eventArgs, String eventName)
    VMBase.OnPropertyChanged(String& propertyName) line 322

In EventManagerService here https://github.com/brminnick/AsyncAwaitBestPractices/blob/master/Src/AsyncAwaitBestPractices/WeakEventManager/EventManagerService.cs#L54 you can see it simply calls Invoke. The correct behavior should be to see if it is a dynamic object and call DynamicInvoke when appropriate.

Looking at the coreclr source here: https://github.com/dotnet/runtime/blob/master/src/coreclr/src/System.Private.CoreLib/src/System/Reflection/Emit/DynamicMethod.cs#L628 you can see that because it is a dynamic delegate, invoke is not allowed and so this is the source of the exception.

CanExecuteChanged doesn't fire

Hello!
Thank you very much for your solution!
I tried "simple" version from https://johnthiriet.com/mvvm-going-async-with-async-command/#
But I faced a problem that I solved without understanding...
The issue is CanExecuteChanged didn't fire on ObservableCollection change. I tried
collection.CollectionChanged += (sender, e) => CalculateAsync.RaiseCanExecuteChanged();
But nothing happened.
So the only way I found is to remove the following method
public void RaiseCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); }
from your code and replace
public event EventHandler CanExecuteChanged;
with
public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } }
I'm new with all these things, so I don't really understand what I have changed. It works now but seems with kind of delay in releasing the button after a command
If you have a minute, can you please explain the correct way of fixing my problem, please.
Thanks in advance

Why's MVVMLight's message sent in vain?

Dear @brminnick, after reading #35, I'd like to ask if you'd elaborate on this a bit further?
Because after watching your YT video, I implemented my commands (unifying your awesome IAsyncCommand with customer given requirement of using MVVMLight's RelayCommand) like this:

public IAsyncCommand<string> SaveCommand => _save ?? (_save = new AsyncRelayCommand<string>(async title =>
  {
    await Task.WhenAll(DataService.UpdateTitle(title), CloseDialog(EDITOR_TITLE)).ConfigureAwait(false);
    Messenger.Default.Send(new TitleFinishedMessage(title));
  },
  title => !string.IsNullOrWhiteSpace(title),
  async exception => await ShowError("Title update failed.", exception).ConfigureAwait(false)));

And that's because, using it like

await Task.WhenAll(DataService.UpdateTitle(title), CloseDialog(EDITOR_TITLE, new TitleFinishedMessage(title)))
          .ConfigureAwait(false);

with a centralized Messenger.Default.Send(message) inside CloseDialog() still sends the message, but in vain then.

I've already verified it's not because of using continueOnCapturedContext's value for MVVMLight's constructors keepTargetAlive parameter, too: passing true instead (ignoring potential memory leak for this) still doesn't fix the "message in vain" problem.

Can you help me, understanding this mystery?

Windows 11 - Smart App Control

Windows 11 introduced Smart App Control. One of the security checks for apps is ensuring that the app and its binaries are signed. Because AsyncAwaitBestPractices does not sign its binaries, software using it will be blocked from running when Smart App Control is enabled.

Please sign DLLs produced for AsyncAwaitBestPractices.

https://support.microsoft.com/en-us/topic/what-is-smart-app-control-285ea03d-fa88-4d56-882e-6698afdb7003

AsyncAwaitBestPractices (unsigned):
image

Microsoft dll (signed):
image

Why is there an in-keyword on a bool parameter

Hi,

trying to learn from your code (as well as from your presentation: liked them a lot :) ) I found things I can't wrap my head around. So maybe you can enlighten me.

Considering The ‘in’-modifier and the readonly structs in C# and of that specifically the 2nd point of the conclusion:

Conclusion

  • The readonly structs are very useful from the design and the performance points of view.
  • If the size of a readonly struct is bigger than IntPtr.Size you should pass it as an in-parameter for performance reasons.
  • You may consider using the in-parameters for reference types to express your intent more clearly.
  • You should never use a non-readonly struct as the in parameters because it may negatively affect performance and could lead to an obscure behavior if the struct is mutable.

I got two questions:

  1. Why does the in bool continueOnCapturedContext or the in ConfigureAwaitOptions configureAwaitOptions parameter use the in-Keyword? Or better, why do you have overloads with them? Both are passed around as 32bit values, however the IntPtr.Size is at least as large or most likely even 64bit. Furthermore, calling the async method of HandleSafeFireAndForget will copy the value and not the reference anyway.
  2. What is the point of using an in-keyword on the in Action<TException>? onException? Yes, the compiler would not let anyone assign a reference to a different action with in the called method. Me calling the method and passing the action, I don't care what is done to the reference once I passed it. And again, all is lost once the HandleSafeFireAndForget-method is called.

Extend WeakEvent Manager to use Delegate

Add the following APIs to WeakEventManager:

public void AddEventHandler(Delegate handler, [CallerMemberName] string eventName = "");
public void RemoveEventHandler(Delegate handler, [CallerMemberName] string eventName = "");

This allows WeakEventManager to be used with Events that don't use EventHandler, like INotifyPropertyChanged:

class BaseViewModel : INotifyPropertyChanged
{
    readonly WeakEventManager _propertyChangedEventManager = new WeakEventManager();

    event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
    {
        add => _propertyChangedEventManager.AddEventHandler(value);
        remove => _propertyChangedEventManager.RemoveEventHandler(value);
    }

    protected void OnPropertyChanged([CallerMemberName]string propertyName = "") => _propertyChangedEventManager?.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(INotifyPropertyChanged.PropertyChanged));
}

Execute parameter ignored

Example

// I can't provide argument for async command
public IAsyncCommand Login => new AsyncCommand(async () => 
        {
            await Authenticate(new Credentials(AccessKey, SecretKey));
        }, _ => CanLogin, exception => Notificator.Critical(exception));

// But I can do it for sync version

 public ICommand OpenLink => new DelegateCommand(url => 
        {
            Process.Start(new ProcessStartInfo(url as string));
        });

Fix 1: Func should have object argument and return Task.

Func<Task> _execute;
// Should be 
Func<object, Task> _execute;

Fix 2: ignored parameter for _execute command.

void ICommand.Execute(object parameter)
    {
      // added (parameter)
      this._execute(parameter).SafeFireAndForget(in this._continueOnCapturedContext, in this._onException);
    }

Is "Sealed" necessary for AsyncCommand?

Hi Brandon and thanks in advance.
Do you remember DependentCommand?
The idea is to re-evaluate the CanExecute when one dependent property changes.
It would be nice to have the same thing here...something like DependentAsyncCommand.
But AsyncCommand is a sealed class, it can't be used as a base class and so it means I have to copy-paste to re-implement what is already inside.
Is there a reason?

Parameter change does not invoke CanExecuteChanged

When the command parameter is updated, the CanExecuteChanged-event is not invoked due to missing link to CommandManager.RequerySuggested.
Working implementation could be something like:

        public event EventHandler? CanExecuteChanged
        {
            add => CommandManager.RequerySuggested += value;
            remove => CommandManager.RequerySuggested -= value;
        }

The above code will execute CanExecute() after the command parameter changed, but is only available in WPF projects. Maybe creating a WPF lib could fix that?

Error when running from signed assembly

I get the following exception when running from a signed assembly. Can we get your code signed?

TIA.

System.IO.FileLoadException: 'Could not load file or assembly 'AsyncAwaitBestPractices.MVVM, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. A strongly-named assembly is required. (Exception from HRESULT: 0x80131044)'

Question: Cancellation

Hi,

first of all thanks for a very useful library :-)

My question is: how would I make an AsyncCommand cancellable? Scenario: cmd is bound to a long running operation, say a big download, that the user should be able to cancel via another command.

I can't seem to find any support for cancellation, but maybe I'm overlooking something?

Cheers & thanks,
MR

SafeFireAndForget is not safe

https://github.com/brminnick/AsyncAwaitBestPractices/blame/main/README.md#L140
This line says that:

`SafeFireAndForget` allows a Task to safely run on a different thread while the calling thread does not wait for its completion.

That couldn't be true, because implementation doesn't switch threads at all

static async void HandleSafeFireAndForget<TException>(ValueTask valueTask, bool continueOnCapturedContext, Action<TException>? onException) where TException : Exception
{
try
{
await valueTask.ConfigureAwait(continueOnCapturedContext);
}
catch (TException ex) when (_onException != null || onException != null)
{
HandleException(ex, onException);
if (_shouldAlwaysRethrowException)
throw;
}
}

Everything prior first await will be executed on caller thread because that's how C# asyncs work (e.g. F# asyncs are different)

Code below will block caller thread for 5 sec before printing, which demonstrate that SafeFireAndForget isn't actually safe.

static async Task BadTask()
{
    Thread.Sleep(5000);
    await Task.CompletedTask;
}

static void Main(string[] args)
{
    BadTask().SafeFireAndForget();
    Console.WriteLine("Hello World!");
}

CanExecute is called only once

I tried to use the package in the WPF project, but it seems that I missed something.

Let say I have the following command:
SaveQuickEditItemCommand = new AsyncCommand(SaveQuickEditItemCommandExecutedAsync, SaveQuickEditItemCommandCanExecute);

It looks like the method
public bool SaveQuickEditItemCommandCanExecute(object o)
is called only once, when my control is launched.

However, if I use a simple ICommand interface instead of IAsyncCommand it works differently: it is called each time I change the value in my control (or probably even each time the control was redrawn, I'm not sure).

How can I force IAsyncCommand to work the same way?

Question about capctured context in AsyncCommand

Hi,

wondering why the default is false for continueOnCapturedContext. I mean, I know thats in general the better choice. But most of commands do something with UI, right? For Example Updating a ListView, showing a busy indicator, etc. Im not very deep in Xamarin Forms binding Engine and not sure about thread synchronization in case the AsyncCommand starts something that runs next to the main thread (And yea, theres not always a new thread, but can).

When you think of most common UI cases, is false a good default? Thanks in advance

bool continueOnCapturedContext = false)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.