29

I have code:

public async Task DeleteColorSchemeAsync(ColorScheme colorScheme)
{
    if (colorScheme == null)
        throw new ArgumentNullException(nameof(colorScheme));

    if (colorScheme.IsDefault)
        throw new SettingIsDefaultException();

    _dbContext.ColorSchemes.Remove(colorScheme);
    await _dbContext.SaveChangesAsync();
}

One code analyzer recommends me to split this method to 2 methods:

Split this method into two, one handling parameters check and the other handling the asynchronous code

Am I correct, when I split this code in the following way?

public async Task DeleteColorSchemeAsync(ColorScheme colorScheme)
{
    if (colorScheme == null)
        throw new ArgumentNullException(nameof(colorScheme));

    if (colorScheme.IsDefault)
        throw new SettingIsDefaultException();

    await DeleteColorSchemeInternalAsync(colorScheme);
}

private async Task DeleteColorSchemeInternalAsync(ColorScheme colorScheme)
{
    _dbContext.ColorSchemes.Remove(colorScheme);
    await _dbContext.SaveChangesAsync();
}

What is different for the compiler? It sees two async methods, what is different from my first variant?

used code tool analyzator: sonarqube

6
  • 2
    I would not split that method into two. What is the reasoning?
    – John Wu
    Commented Jul 5, 2019 at 22:07
  • 2
    For an iterator method, that makes sense because you want the validations to occurr immediately, not on the first call to MoveNext. For an async method, though, it'll run everything up the the await synchronously (to the best of my understanding, anyway), so it doesn't seem like there's any benefit to splitting it like that. Commented Jul 5, 2019 at 22:10
  • I don't understand reason to split. So, not necessary without iterator method?
    – Oleg Sh
    Commented Jul 5, 2019 at 22:19
  • 4
    You are correct, it's not necessary here. The code analysis tool you're using is likely getting misled, because async/await is built on the same basic technique the compiler uses for iterator methods, so your async method looks like an iterator method to it. It would help if you would clarify in your post, as well as by adding a tag, which code analysis tool is producing this message. Commented Jul 5, 2019 at 22:31
  • Analyzing code is a difficult process. Although SonarQube does its best to prevent false positives, the tool cannot prevent them. That is why you can mark messages as a false positive so that next analysis will skip this issue. Commented Jul 6, 2019 at 6:50

2 Answers 2

47

Assuming you wanted to follow the code analysis advice, I would not make the first method async. Instead, it can just do the parameter validation, and then return the result of calling the second:

public Task DeleteColorSchemeAsync(ColorScheme colorScheme)
{
    if (colorScheme == null)
        throw new ArgumentNullException(nameof(colorScheme));

    if (colorScheme.IsDefault)
        throw new SettingIsDefaultException();

    return DeleteColorSchemeInternalAsync(colorScheme);
}

private async Task DeleteColorSchemeInternalAsync(ColorScheme colorScheme)
{
    _dbContext.ColorSchemes.Remove(colorScheme);
    await _dbContext.SaveChangesAsync();
}

All that said, in my opinion there's not really a strong justification to split the method like that. The SonarQube's rule, Parameter validation in "async"/"await" methods should be wrapped is IMHO overly cautious.

The compiler uses the same kind of transformation on async methods as it does for iterator methods. With an iterator method, there is value in doing parameter validation in a separate method, because otherwise it won't be done until the caller tries to get the first element in the sequence (i.e. when the compiler-generated MoveNext() method is called).

But for async methods, all of the code in the method up to the first await statement, including any parameter validation, will be executed on the initial call to the method.

The SonarQube rule appears to be based on a concern that until the Task is observed, any exception generated in the async method won't be observed. Which is true. But the typical calling sequence of an async method is to await the returned Task, which will observe the exception immediately on completion, which of course occurs when the exception is generated, and will happen synchronously (i.e. the thread won't be yielded).

I admit that this is not hard-and-fast. For example, one might initiate some number of async calls, and then use e.g. Task.WhenAll() to observe their completions. Without immediate parameter validation, you would wind up starting all of the tasks before realizing that one of the calls was invalid. And this does violate the general principle of "fail-fast" (which is what the SonarQube rule is about).

But, on the other hand, parameter validation failures are almost always due to user code incorrectness. I.e. they don't happen because of data input problems, but rather because the code was written incorrectly. "Fail-fast" is a bit of a luxury in that context; what's more important, to me anyway, is that the code be written in a natural, easy-to-follow way, and I'd argue that keeping everything in one method achieves that goal better.

So in this case, the advice being given by SonarQube isn't necessary to follow. You can just leave your async method as a single method, the way you had it originally without harming the code. Even more so than the iterator method scenario (which has similar arguments pro and con), there is IMHO just as much reason to leave the validation in the async method as to remove it to a wrapper method.

But if you do choose to follow SonarQube's advice, the example I provided above is IMHO a better approach than what you have (and indeed, is more in line with the detailed advice on SonarQube's documentation).

I will note that in fact, there's an even simpler way to express the code:

public Task DeleteColorSchemeAsync(ColorScheme colorScheme)
{
    if (colorScheme == null)
        throw new ArgumentNullException(nameof(colorScheme));

    if (colorScheme.IsDefault)
        throw new SettingIsDefaultException();

    _dbContext.ColorSchemes.Remove(colorScheme);
    return _dbContext.SaveChangesAsync();
}

I.e. don't make the implementation async at all. Your code doesn't need async because there's only one await and it occurs at the very end of the method. Since your code doesn't actually need control returned to it, there's not actually any need to make it async. Just do all the synchronous stuff you need to do (including parameter validation), and then return the Task you'd otherwise have awaited.

And, I'll note as well, this approach addresses both the code analysis warning, and keeps the implementation simple, as a single method with parameter validation built-in. Best of both worlds. :)

3
  • This is a good answer. However, you should likely be using Task.FromException to return a synchronously canceled or faulted task. ... however, because they are preconditions, they shouldn't be caught anyway, so i guess its neither here or there
    – TheGeneral
    Commented Feb 15, 2021 at 0:34
  • @00110001: I guess Task.FromException() would be fine here too, but I'm not sure I'd agree that the code should use that. It's not consistent with the broader theme of "fail fast" (since it'd wrap the exception in a task instead of just throwing it directly), and would be counter-productive in scenarios where the caller doesn't immediately check the task on return (e.g. tasks are being accumulated into a collection, to be processed later...you could wind up having another dozen or hundred tasks started only to abandon them because the first one wasn't even invoked correctly). Commented Feb 15, 2021 at 1:31
  • 1
    I think this is overall bad advice and should not be followed by default. Firing a set of tasks and then awaiting them all is a very known and common pattern that is directly affected by this: failing earlier in that scenario can make a big difference. Additionally, the suggestion to just return tasks from the methods directly (without awaiting them) can severely impact how stacktraces are presented, and make it much more difficult to debug problems since whenever you just "delegate" one method to the other's result like this, the method will be omitted from the trace.
    – julealgon
    Commented Aug 24, 2021 at 20:51
17

Am I correct, when I split this code in the following way?

No. The correct way to split them would be like this:

public Task DeleteColorSchemeAsync(ColorScheme colorScheme)
{
    if (colorScheme == null)
        throw new ArgumentNullException(nameof(colorScheme));

    if (colorScheme.IsDefault)
        throw new SettingIsDefaultException();

    return DeleteColorSchemeInternalAsync(colorScheme);
}

private async Task DeleteColorSchemeInternalAsync(ColorScheme colorScheme)
{
    _dbContext.ColorSchemes.Remove(colorScheme);
    await _dbContext.SaveChangesAsync();
}

(note that the entry method is not async in this case).

Or like this, using the newer local functions:

public Task DeleteColorSchemeAsync(ColorScheme colorScheme)
{
    if (colorScheme == null)
        throw new ArgumentNullException(nameof(colorScheme));

    if (colorScheme.IsDefault)
        throw new SettingIsDefaultException();

    return DeleteColorSchemeAsync();

    async Task DeleteColorSchemeAsync()
    {
        _dbContext.ColorSchemes.Remove(colorScheme);
        await _dbContext.SaveChangesAsync();
    }
}

The reason this rule exists is to make sure you throw as soon as possible on usage exceptions. If you don't split the logic and leave validation inside the async method, the exception will only be thrown when someone awaits your returned task, which may not happen immediately depending on usage.

One very common flow where throwing early would be beneficial, is when you want to fire multiple tasks concurrently and await for their completion. Since on this flow your await action happens after the tasks are fired, you'll get an exception that is potentially very far from the actual point of the call, making it unnecessarily harder to debug.

The accepted answer also proposes returning the task directly in cases where you have only one async operation to do and that is the result value, however that introduces significant problems when debugging code, since your method is omitted from the stacktrace, making it harder to navigate in the entire flow. Here is a video that discusses this problem in more depth: https://youtu.be/Q2zDatDVnO0?t=327

Returning the task directly without awaiting it should only be done for extremely simple "relay"-type methods, where there is very little relevant logic in the parent method.

I'd advise following the rule at all times.

1
  • actually this is the code that SonarQube proposes, without async in the first method, not sure if that wasn't correct before, but it is now. Still, IMO doesn't justify the 'Major' classification of this issue.
    – mikus
    Commented Sep 26, 2022 at 15:05

Not the answer you're looking for? Browse other questions tagged or ask your own question.