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.
Overview
This issue is about discussing ways to improve how
AsyncRelayCommandtypes 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 throughExecute(ie. through a synchronous API), theTaskobject is not awaited. This is by design, as it allows exceptions to be observable through theExecutionTaskproperty afterwards, or to otherwise bubble up toTaskScheduler.UnobservedTaskExceptionto enable centralized logging. Currently, if someone wants to await a task,ExecuteAsyncneeds to be called instead. This also has the characteristic that callingExecutewill 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:
This would then be used both in constructors for
AsyncRelayCommand, as well as[ICommand].Worth noting, this would replace the current
bool allowConcurrentExecutionsparameters in the constructors, as instead we would just take anAsyncRelayCommandOptions optionsparameter 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:
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:
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
PropertyChangedon 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.