Enforcing asynchronous code good practices using a Roslyn analyzer
In a previous post I wrote about one rule of Meziantou.Analyzer to help using CancellationToken
correctly. Meziantou.Analyzer is an open-source Roslyn analyzer I wrote to enforce some good practices in C# in terms of design, usage, security, performance, and style.
In this post, we'll explore the rules that help when writing asynchronous code in C#.
#How to install the Roslyn analyzer
The recommended way to use the analyzer is to add the Meziantou.Analyzer
NuGet package to your project:
dotnet add package Meziantou.Analyzer
Or you can add the following <PackageReference>
to the csproj:
<Project>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.149">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
#MA0004 - Use ConfigureAwait when awaiting a task
You should use ConfigureAwait(false)
except when you need to use the current SynchronizationContext
, such as in a WinForm, WPF, or ASP.NET context.
If you want to know more about ConfigureAwait
, you should Stephen Toub's post: ConfigureAwait FAQ
async Task Sample()
{
await Task.Delay(1000); // 👈 MA0004
await Task.Delay(1000).ConfigureAwait(false); // ok
await Task.Delay(1000).ConfigureAwait(true); // ok
await foreach (var item in AsyncEnumerable()) { } // 👈 MA0004
await foreach (var item in AsyncEnumerable().ConfigureAwait(false)) { } // ok
await foreach (var item in AsyncEnumerable().WithCancellation(token)) { } // 👈 MA0004
await foreach (var item in AsyncEnumerable().WithCancellation(token).ConfigureAwait(false)) { } // ok
await using var disposable = new AsyncDisposable(); // 👈 MA0004
await using var disposable = new AsyncDisposable().ConfigureAwait(false); // ok
await using (var disposable = new AsyncDisposable()) { } // 👈 MA0004
await using (var disposable = new AsyncDisposable().ConfigureAwait(false)) { } // ok
}
The analyzer is automatically disabled in a WPF, WinForms, or ASP.NET Core context. Indeed in WPF, WinForms, or Blazor, you may want to continue the execution on the current SynchronizationContext
, this means using ConfigureAwait(true)
. In ASP.NET Core MVC, there is no SynchronizationContext
, so using ConfigureAwait(false)
is useless. Thus, to avoid useless code, the rule is disabled when it detects one of these contexts.
using System.Windows.Controls;
public class MyControl : Control // WPF Control
{
public async Task Sample()
{
await Task.Delay(1000); // ok in a WPF context
await Task.Delay(1000).ConfigureAwait(false); // ok
await Task.Delay(1000); // 👈 MA0004 because there is a previous await that uses ConfigureAwait(false)
}
}
#MA0022 - Return Task.FromResult instead of returning null
You should not return null
task as awaiting them would throw a NullReferenceException
. It may occur if you remove the async
keyword without adapting the return value. The analyzer prevents you from doing this mistake.
await Sample(); // NullReferenceException as Sample() returns null.
Task<object> Sample()
{
return null; // 👈 MA0022
}
async Task<object> Sample()
{
return null; // ok as the method is async, so the returned task is not null
}
Task<object> Sample()
{
return Task.FromResult(null); // ok
}
#MA0040 - Flow the cancellation token when available
When possible, you should use a CancellationToken to allow call sites to cancel the current operation. I've already written a complete post about this rule here, so I won't show all the supported cases.
public void MyMethod(CancellationToken cancellationToken = default) { }
public void Sample(CancellationToken cancellationToken)
{
MyMethod(); // 👈 MA0040: Missing cancellation token (cancellationToken)
MyMethod(cancellationToken); // ok
}
public void MyMethod(CancellationToken cancellationToken = default) { }
public void Sample(HttpContext httpContext)
{
MyMethod(); // 👈 MA0040: Missing cancellation token (httpContext.RequestAborted)
MyMethod(cancellationToken); // ok
}
#MA0079 - Flow the cancellation token in await foreach using WithCancellation
When using async foreach
you should provide a CancellationToken
using WithCancellation(cancellationToken)
.
public async Task MyMethod(IAsyncEnumerable<int> enumerable, CancellationToken cancellationToken)
{
await foreach(var item in enumerable) // 👈 MA0079: Flow CancellationToken using WithCancellation(cancellationToken)
{
}
await foreach(var item in enumerable.WithCancellation(cancellationToken)) // ok
{
}
}
#MA0100 - Await tasks before disposing resources
You should await a task before exiting a using block. Otherwise the task may use disposed resources and crash.
Task Demo1()
{
using var scope = new Disposable();
return Task.Delay(1); // MA0100, you must await the task before disposing the scope
}
#MA0042 - Do not use blocking calls in an async context
In an async method, you should not use any blocking method such as Task.Wait
, Task.Result
, or Thread.Sleep()
.
async Task Sample()
{
Task<string> task;
task.Wait(); // 👈 MA0042, use await task
task.GetAwaiter().GetResult(); // 👈 MA0042, use await task
_ = task.Result; // 👈 MA0042, use await task
Thread.Sleep(1000); // 👈 MA0042, use await Task.Delay(1000)
}
This analyzer also detects when you use synchronous methods when equivalent asynchronous methods are available. The analyzer search for methods having the following properties:
- It should have the same name or have the
Async
suffix - It should return a
Task
or aValueTask
- It should have the same parameters and optionally an additional
CancellationToken
parameter
async Task Sample()
{
File.WriteAllText("author.txt", "meziantou"); // 👈 MA0042, use await File.WriteAllTextAsync
Wait(); // 👈 MA0042, use await WaitAsync()
}
void Wait() { }
Task WaitAsync(CancellationToken cancellationToken) { }
#MA0032 / MA0045 / MA0080 - Change the method to be async
These analyzers detect the same cases as the previous rules (MA0040/MA0042/MA0079) except it doesn't require the containing method to return a Task
nor to have an available CancellationToken
. This means these rules report locations when you need to change the signature of the containing method to apply the fix. These rules can be useful when you want to migrate your codebase to use asynchronous code.
These rules are disabled by default because they can be very noisy and report lots of false positives. First, you need to enable them by creating or editing the .editorconfig
file:
dotnet_diagnostic.MA0032.severity = suggestion
dotnet_diagnostic.MA0045.severity = suggestion
dotnet_diagnostic.MA0080.severity = suggestion
public void Sample()
{
// 👇 MA0045, use Task.Delay(1).
// It requires to change the signature of the containing method to be async
Thread.Sleep(1);
// 👇 MA0032, use a CancellationToken but there is no token available in the current context.
// It requires to change the signature of containing method accept a CancellationToken
MyMethod();
// 👇 MA0080, use a CancellationToken using WithCancellation() but there is no token available in the current context.
// It requires to change the signature of containing method accept a CancellationToken
await foreach(var item in enumerable) { }
}
public void MyMethod(CancellationToken cancellationToken = default) { }
#MA0129 - Await task in using statement
A Task
doesn't need to be disposed but implements IDisposable
. When used in a using
statement, most of the time, developers forgot to await it. This can happen when you convert a synchronous method to an asynchronous implementation.
Task<IDisposable> task = ...;
// 👇 MA0129, as the value is not disposed
using(task) { }
// ✔️ Valid as the value is actually disposed
using(await task) { }
#MA0134 - Observe result of async calls
The result of awaitable method should be observed by using await
, Result
, Wait
, or other methods.
Note: CS4014 is similar but only operate in async
methods. MA0134 also operates in non-async methods.
void Sample()
{
// 👇 MA0134 as the task is not awaited
Task.Delay(1);
}
#MA0147 - Avoid async void method for delegate
// 👇 MA0147 as the delegate is not expecting an async method
Foo(async () => {});
void Foo(System.Action action) => throw null;
// ✔️ Valid
FooAsync(async () => {});
Task FooAsync(Func<Task> action) => throw null;
#MA0155 - Do not use async void methods
This rule must be enabled in the .editorconfig
file as it could report cases that cannot be fixed automatically. For instance, event handlers are often synchronous, so you need to use async void
.
dotnet_diagnostic.MA0155.severity = warning
// 👇 MA0155
async void Method()
{
// 👇 MA0155
async void LocalFunction() { }
}
#MA0152 - Use Unwrap instead of using await twice
Prefer using Unwrap
instead of using await
twice
Task<Task> t;
// 👇 MA0152
await await t;
// ✔️ Valid
await t.Unwrap();
#Bug / Feature requests
If you find a bug or think that a rule can be improved, feel free to open an issue on the GitHub repository: https://github.com/meziantou/Meziantou.Analyzer
Do you have a question or a suggestion about this post? Contact me!