Detecting missing CancellationToken using a Roslyn Analyzer

 
 
  • Gérald Barré

Recently, Marc Gravell asked on Twitter if an analyzer is available to detect when a method is called without specifying a CancellationToken whereas there is one available in the current context (local variable or parameter).

C#
using System.Threading;

namespace ConsoleApp
{
    public class MyTestClass
    {
        public void MyMethodWithDefault(CancellationToken ct = default) { }

        public void MyMethod(CancellationToken ct)
        {
            MyMethodWithDefault();   // 👈 Missing cancellation token
            MyMethodWithDefault(ct); // ok
        }
    }
}

Source: Twitter

The Roslyn Analyzer I develop — Meziantou.Analyzer — contains a rule to detect missing CancellationToken: MA0040 - Use a cancellation token when available.

First, you need to install the analyzer to your project

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Meziantou.Analyzer" Version="1.0.444">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>

Then, it will process your project and detect call sites where you could use a CancellationToken. Here're a few cases where the analyzer raises a diagnostic:

  • The method has an optional parameter of type CancellationToken that is not provided

    C#
    public void MyMethodWithDefault(CancellationToken cancellationToken = default) { }
    
    public void MyMethod(CancellationToken cancellationToken)
    {
        MyMethodWithDefault(); // 👈 MA0040: Missing cancellation token (cancellationToken)
    }

  • The method has an overload with an additional parameter of type CancellationToken

    C#
    public void MyMethodWithDefault() { }
    public void MyMethodWithDefault(CancellationToken cancellationToken) { }
    
    public void MyMethod(CancellationToken cancellationToken)
    {
        MyMethodWithDefault(); // 👈 MA0040: Missing cancellation token (cancellationToken)
    }

  • A CancellationToken is available through a CancellationTokenSource:

    C#
    public void MyMethodWithDefault(CancellationToken ct = default) { }
    
    public void MyMethod()
    {
        var cts = new CancellationTokenSource();
        MyMethodWithDefault(); // 👈 MA0040: Missing cancellation token (cts.Token)
    }

  • A CancellationToken is available through a property of the current class or a base class:

    C#
    public CancellationToken CancellationToken { get; set; }
    
    public void MyMethodWithDefault(CancellationToken ct = default) { }
    
    public void MyMethod()
    {
        MyMethodWithDefault(); // 👈 MA0040: Missing cancellation token (CancellationToken)
    }

  • A CancellationToken is available from an HttpContext in ASP.NET Core (RequestAborted):

    C#
    public class MyController : ControllerBase
    {
        public void MyMethodWithDefault(CancellationToken ct = default) { }
    
        public void Get()
        {
            MyMethodWithDefault(); // 👈 MA0040: Missing cancellation token (HttpContext.RequestAborted)
        }
    }

  • A CancellationToken is available from a gRPC ServerCallContext (ServerCallContext.CancellationToken):

    C#
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        MyMethodWithDefault(); // 👈 MA0040: Missing cancellation token (context.CancellationToken)
    
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
    
    public void MyMethodWithDefault(CancellationToken ct = default) { }

  • A CancellationToken is available from a custom object available in the current scope:

    C#
    public void MyMethodWithDefault(CancellationToken ct = default) { }
    
    public void MyMethod(MyContext context)
    {
        MyMethodWithDefault(); // 👈 MA0040: Missing cancellation token (context.CancellationToken)
    }
    
    class MyContext
    {
        public CancellationToken CancellationToken { get; set; }
    }

  • A CancellationToken is available in an await foreach method:

    C#
    static async IAsyncEnumerable<int> RangeAsync(int start, int count, [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        for (int i = 0; i < count; i++)
        {
            await Task.Delay(i, cancellationToken).ConfigureAwait(false);
            yield return start + i;
        }
    }
    
    public async Task MyMethod(CancellationToken cancellationToken)
    {
        await foreach(var item in RangeAsync(0, 10)) // 👈 MA0040: Missing cancellation token (cancellationToken)
        {
        }
    }
    
    public async Task MyMethod(IAsyncEnumerable<int> enumerable, CancellationToken cancellationToken)
    {
        await foreach(var item in enumerable) // 👈 MA0079: Flow CancellationToken using WithCancellation(cancellationToken)
        {
        }
    }

If multiple cancellation tokens are available in the current context, there are all listed in the message so you can choose which one you want to use:

C#
public CancellationToken Property { get; set; }

public void MyMethodWithDefault(CancellationToken ct = default) { }

public void MyMethod(CancellationToken parameter)
{
    var cts = new CancellationTokenSource();
    MyMethodWithDefault(); // 👈 MA0040: Missing cancellation token (parameter, Property, cts.Token)
}

The default severity of the rule is Info. You can change it to warning or error if wanted by using a .editorconfig file:

# .editorconfig
dotnet_diagnostic.MA0040.severity = warning

Note that Microsoft has recently implemented a similar analyzer (CA2016) which is currently in preview. But this analyzer is limited compared to the one shown in this post. For instance, it only detects missing CancellationToken when this parameter is located in the last position. That's being said, I'm confident that the analyzer will be improved in the future and will be better than the one I developed.

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub