No Async Suffix

Component: NServiceBus

Starting with NServiceBus Version 6, all APIs that contain potentially IO bound code are Async. Some examples include:

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

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

Reference Article: Asynchronous Programming with async and await.

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 sync and async versions of the API. Therefore the need to add the async suffix does not apply.

The noise caused in API usage

There is already non-trivial verbosity that is added to a codebase when async is 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.

So 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 Read: Hungarian notation Disadvantages.

Async APIs should be identifiable in code

One of the arguments for the Async suffix is that all async 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 in several of the above examples are warnings and not 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 two possible solutions, writing a Roslyn analyzer or writing a unit test using Mono Cecil.

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, .ConfigureAwait() usage that is required.

Other libraries with no Async suffix.

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

Verify correct Task usage using a unit test

This scenario uses Mono Cecil to interrogate the IL of an assembly to verify correct usage of Task based method calls. In this case the code verifies that there is at least one usage of the Task instance returns from a method.

Missing Task Usage Detector

Helper that detects and fails for incorrect Task usage.

public static class MissingTaskUsageDetector
{

    public static void CheckForMissingTaskUsage(string assemblyPath)
    {
        var readerParameters = new ReaderParameters
        {
            ReadSymbols = true
        };
        var moduleDefinition = ModuleDefinition.ReadModule(assemblyPath, readerParameters);
        var errors = new List<string>();
        foreach (var type in moduleDefinition.GetTypes())
        {
            foreach (var method in type.Methods)
            {
                if (!method.HasBody)
                {
                    continue;
                }
                errors.AddRange(method.CheckForMissingTaskUsage());
            }
        }
        if (errors.Count > 0)
        {
            Assert.Fail(string.Join(Environment.NewLine, errors));
        }
    }

    public static IEnumerable<string> CheckForMissingTaskUsage(this MethodDefinition method)
    {
        MethodReference taskMethod = null;
        foreach (var instruction in method.Body.Instructions)
        {
            if (taskMethod != null && instruction.OpCode == OpCodes.Pop)
            {
                var declaringType = method.GetDeclaringType();
                var lineNumber = instruction.Previous.GetNearLineNumber();
                yield return $"Type '{declaringType.FullName}' contains a call to '{taskMethod.DeclaringType.Name}.{taskMethod.Name}' near line {lineNumber} where no usage of the returned Task is detected.";
            }
            taskMethod = null;
            var operand = instruction.Operand as MethodReference;
            if (operand != null && operand.ReturnsTask())
            {
                taskMethod = operand;
            }
        }
    }

    static bool ReturnsTask(this MethodReference method)
    {
        var returnType = method.ReturnType;
        return returnType != null &&
               returnType.IsTaskType();
    }

    static bool IsTaskType(this TypeReference type)
    {
        if (type.Namespace != "System.Threading.Tasks")
        {
            return false;
        }
        if (type.Name == "Task")
        {
            return true;
        }
        if (type.Name.StartsWith("Task`1"))
        {
            return true;
        }
        return false;
    }

    static TypeDefinition GetDeclaringType(this MethodDefinition method)
    {
        var type = method.DeclaringType;
        while (type.IsCompilerGenerated() && type.DeclaringType != null)
        {
            type = type.DeclaringType;
        }
        return type;
    }

    static bool IsCompilerGenerated(this ICustomAttributeProvider value)
    {
        return value.CustomAttributes
            .Any(a => a.AttributeType.Name == "CompilerGeneratedAttribute");
    }

    static string GetNearLineNumber(this Instruction instruction)
    {
        while (true)
        {
            if (instruction.SequencePoint != null)
            {
                return instruction.SequencePoint.StartLine.ToString();
            }

            instruction = instruction.Previous;
            if (instruction == null)
            {
                return "?";
            }
        }
    }
}

Using the detector in a unit test

The above helper can then be called from any unit test and passed a path to an assembly to verify.

[Test]
public void TestForMissingTaskUsage()
{
    MissingTaskUsageDetector.CheckForMissingTaskUsage(pathToAssembly);
}

The resulting error will be:

Type 'ClassName' contains a call to 'ClassWithAsync.MethodWithAsync' near line 21 where no usage of the returned Task is detected.

IgnoreTaskExtensions

In some cases it may be desirable to ignore the returned Task value. In this case an extension method can be used to explicitly accept that the Task return value should be ignored.

public static class TaskExtensions
{
    public static void IgnoreTask(this Task task)
    {
    }
}

Using IgnoreTask extension method.

public static void ExplicitlyIgnoreTask()
{
    var writer = new StreamReader("stub");
    // Note the returned instance is ignored
    writer.ReadLineAsync().IgnoreTask();
}

Last modified