Getting Started
Architecture
NServiceBus
Transports
Persistence
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Samples

No Async Suffix

Component: NServiceBus

This is part of the NServiceBus Upgrade Guide from Version 5 to 6, which also includes the following individual upgrade guides for specific components:

Feature Details
Transports
Persistence
Hosting
Other

Starting with NServiceBus version 6, all APIs that contain potentially I/O-bound code are asynchronous. Some examples include:

None of the above APIs have an Async suffix as recommended by the Microsoft convention, which states:

The name of an async method, by convention, ends with an Async suffix.

The decision not to adopt the Async suffix in NServiceBus API is intentional for several reasons.

Reason for No Async Suffix

No requirement for conflicting overloads

The Async suffix convention was adopted by necessity in .NET CLR since async APIs were added in a non-breaking version. Since C# cannot have overloads that differ only by return type, the new async APIs needed to have a different name, hence the Async suffix was used.

Adding async to NServiceBus version 6 in itself is a breaking change. In comparison to the .NET CLR APIs, NServiceBus has no requirement to support both synchronous and asynchronous versions of the API. Therefore the need to add the Async suffix does not apply.

Noise in API usage

There is already non-trivial verbosity that is added to a codebase when asynchronous principles are adopted. For example .ConfigureAwait() additions, async and await keywords, and Task<T> return values.

NServiceBus APIs do not follow Hungarian notation

No other NServiceBus APIs follow Hungarian notation. For example:

  • Methods are not suffixed with the name of the type they return.
  • Classes are not suffixed with "Instance" or "Static".
  • Members are not suffixed access modifier names such as "Protected" or "Public".

All these things can be inferred by the IDE (e.g. Visual Studio) and the compiler, and appropriate IntelliSense and compiler messages are provided to the developer.

Therefore, in deciding on the adoption of the Async suffix it was necessary to choose between consistency with certain external .NET APIs or naming consistency within NServiceBus.

Related article: Hungarian notation disadvantages.

Async APIs should be identifiable in code

One of the arguments for the Async suffix is that all asynchronous methods should be clearly identifiable in code so as to prevent misuse of that API. However, the compiler is very efficient at identifying incorrect async keyword usage and providing appropriate feedback to the developer. Some possible misuses are listed below with the associated compiler information.

Missing return task

When a Task method calls an async method but neglects to await that method.

public static Task TaskMethodMissingAwait()
{
    var writer = new StreamWriter("stub");
    writer.WriteLineAsync();
}

Results in Compiler Error CS0161

Async method with missing await

When an async Task method calls an async method but neglects to await that method.

public static async Task AsyncMethodMissingAwait()
{
    var writer = new StreamWriter("stub");
    writer.WriteLineAsync();
}

Results in Compiler Warning CS4014

Missing a single await

When an async Task method awaits one async method but neglects to await another.

public static async Task AsyncMethodMissingOneAwait()
{
    var writer = new StreamWriter("stub");
    writer.WriteLineAsync();
    await writer.WriteLineAsync();
}

Results in Compiler Warning CS4014

Treat warnings as errors

Note that several of the above examples contain warnings rather than errors. As such, it is necessary to either treat all warnings as errors or nominate specific warnings to be treated as errors via Errors and Warnings.

Cases not detected by the compiler

There are some cases that are not detected by the compiler. For example:

public static Task MissingTaskUsage1()
{
    var writer = new StreamReader("stub");

    // Note the returned instance is not used
    writer.ReadLineAsync();

    return writer.ReadLineAsync();
}

public static void MissingTaskUsage2()
{
    var writer = new StreamReader("stub");

    // Note the returned instance is not used
    writer.ReadLineAsync();
}

In these scenarios, there are other possible solutions, such as writing a Roslyn analyzer.

Async not necessary when reading code

The above examples show how difficult it is to incorrectly use async APIs. As such, async API usage is clearly identifiable in code by the associated await usage that is required.

Other libraries with no Async suffix.

Other libraries are also taking the same approach. For example: