Skip to content

Ability to customize exception flow in AsyncRelayCommand types #262

Description

@Sergio0694

Overview

This issue is about discussing ways to improve how AsyncRelayCommand types handle exceptions. Specifically, as discussed with @brminnick and as people have brought up in the past as well (eg. #22, #175, #251, etc.), the way async relay commands currently work is that if you invoke them through Execute (ie. through a synchronous API), the Task object is not awaited. This is by design, as it allows exceptions to be observable through the ExecutionTask property afterwards, or to otherwise bubble up to TaskScheduler.UnobservedTaskException to enable centralized logging. Currently, if someone wants to await a task, ExecuteAsync needs to be called instead. This also has the characteristic that calling Execute will not crash the app if the command fails.

Now, there are cases where all this is desireable, but there might also be cases where developers would want the exception to just be reported immediately and crash the app, for instance to more easily spot errors and to avoid the app getting into an inconsistent state, in case an exception was not planned to be raised from the wrapped methods at all.

Because of this, we're thinking about ways we could expose this additional flexibility for consumers.

This issue is meant to be a place to discuss this and the various API proposals related to this.

cc. @michael-hawker @Arlodotexe

API breakdown

One possible way to go could be to introduce something like this:

namespace CommunityToolkit.Mvvm.Input;

[Flags]
public enum AsyncRelayCommandOptions
{
    Default,
    AllowConcurrentExecution,
    FlowExceptionsToThreadPool,
    FlowExceptionsToSynchronizationContext,
    // Potentially more options in the future
}

This would then be used both in constructors for AsyncRelayCommand, as well as [ICommand].

Worth noting, this would replace the current bool allowConcurrentExecutions parameters in the constructors, as instead we would just take an AsyncRelayCommandOptions options parameter to specify all options. This way, consumers would have a single way to customize the behavior of the async command on all supported aspects.

As far as the source generator approach goes, we could do something like:

// Before
[ICommand(AllowConcurrentExecutions = true)]
private async Task DoSomethingAsync()
{
    // ...
}

// After
[ICommand(Options = AsyncRelayCommandOptions.AllowConcurrentExecutions)]
private async Task DoSomethingAsync()
{
    // ...
}

This is a bit more verbose than before. It might be ok, but I'm also open to suggestions.

Overall, using something like this would give us the following benefits:

  • Everyone would be happy, as we could support all scenarios we wanted
  • Having some options would give us room to potentially add more flags in the future without breaking or needing new APIs

Open questions

Should the default be the same behavior as today, or should the default be to rethrow in some specific way?

Alternatives

There isn't really a good alternative to have the app crash today. I mean you could do that but it'd be incredibly verbose, as you'd need to register a handler for PropertyChanged on the command, check whether the task changed, get it, check if it faulted, then post to the synchronization context you need and from there re-throw the exceptions. Not really ideal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature request 📬A request for new changes to improve functionalitymvvm-toolkit 🧰Issues/PRs for the MVVM Toolkitopen discussion ☎️An issue open for active community discussion

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions