I've had this warning pop up in SonarQube but I don't understand why wrapping the validation is desirable.
I've read the following questions but neither appears to explain clearly WHY it is better?
Parameter validation in "async"/"await" methods should be wrapped
Validate parameters in async method
In the example
public async Task DoSomethingAsync(string param){
if(string.IsNullOrEmpty(param)
{
throw new ArgumentException("Param is blank");
}
await DoSomethingElseAsync(param);
}
Why would this be executed any differently to
public Task DoSomethingAsync(string param){
if(string.IsNullOrEmpty(param)
{
throw new ArgumentException("Param is blank");
}
return doSomethingElseAsync(param);
}
Given that both implementations would need to be awaited by the caller, would there actually be any guarantee that the validation in the 2nd implementation would be executed immediately rather than be delayed like any other async method?
An asynchronous method is converted by the compiler to a state machine.
When the method is invoked, the state machine is instantiated. And only then, the code before the first await
is invoked.
If you have this method:
private static async Task<int> GetValueAsync(object o)
{
if (o is null) throw new ArgumentNullException(nameof(o));
await Task.Delay(1);
return 0;
}
The generated code will be:
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <GetValueAsync>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder<int> <>t__builder;
public object o;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
int result;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
if (o == null)
{
throw new ArgumentNullException("o");
}
awaiter = Task.Delay(1).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
result = 0;
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult(result);
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
<>t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(<GetValueAsync>d__0))]
private static Task<int> GetValueAsync(object o)
{
<GetValueAsync>d__0 stateMachine = default(<GetValueAsync>d__0);
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.o = o;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
If you validate the parameters in a non-async method:
private static Task<int> GetValueAsync(object o)
{
if (o is null) throw new ArgumentNullException(nameof(o));
return GetValueAsyncImpl(o);
static async Task<int> GetValueAsyncImpl(object o)
{
await Task.Delay(1);
return 0;
}
}
The generated code will be:
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <<GetValueAsync>g__GetValueAsyncImpl|0_0>d : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder<int> <>t__builder;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
int result;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = Task.Delay(1).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
result = 0;
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult(result);
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
<>t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
private static Task<int> GetValueAsync(object o)
{
if (o == null)
{
throw new ArgumentNullException("o");
}
return <GetValueAsync>g__GetValueAsyncImpl|0_0(o);
}
[AsyncStateMachine(typeof(<<GetValueAsync>g__GetValueAsyncImpl|0_0>d))]
[CompilerGenerated]
internal static Task<int> <GetValueAsync>g__GetValueAsyncImpl|0_0(object o)
{
<<GetValueAsync>g__GetValueAsyncImpl|0_0>d stateMachine = default(<<GetValueAsync>g__GetValueAsyncImpl|0_0>d);
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
Which does not instantiate the state machine if there are invalide parameter values.
The same happens for iterator methods.