CancelCop.Analyzer
1.52.44
See the version list below for details.
dotnet add package CancelCop.Analyzer --version 1.52.44
NuGet\Install-Package CancelCop.Analyzer -Version 1.52.44
<PackageReference Include="CancelCop.Analyzer" Version="1.52.44"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="CancelCop.Analyzer" Version="1.52.44" />
<PackageReference Include="CancelCop.Analyzer"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add CancelCop.Analyzer --version 1.52.44
#r "nuget: CancelCop.Analyzer, 1.52.44"
#:package CancelCop.Analyzer@1.52.44
#addin nuget:?package=CancelCop.Analyzer&version=1.52.44
#tool nuget:?package=CancelCop.Analyzer&version=1.52.44
<p align="center"> <img src="https://raw.githubusercontent.com/georgepwall1991/CancelCop.Analyzer/main/assets/cancelcop-icon.png" width="96" height="96" alt="CancelCop.Analyzer icon — Roslyn analyzer for CancellationToken and async/await in C#/.NET"> </p>
CancelCop.Analyzer
Compile-time CancellationToken and async/await Roslyn analyzer for C#/.NET — catches missing cancellation propagation, ignored ASP.NET Core RequestAborted, EF Core and HttpClient token gaps, sync-over-async deadlocks, blocking I/O, async void, and resource-lifetime bugs so they fail in the editor and CI, not only at runtime.
Stop shipping async that cannot cancel.
The problem
CancellationToken and correct async/await usage are essential for responsive .NET apps, but cancellation bugs hide across API boundaries. A public method without a token, an HttpClient or EF Core call that ignores the caller's token, a controller that never sees RequestAborted, a timeout CTS that drops the parent token, or a .Result / Thread.Sleep inside async code often compiles cleanly and only fails under load, shutdown, or client disconnect.
Runtime review and occasional CA rules miss what a dedicated cancellation-and-async analyzer can prove from your call sites.
What it catches
CancelCop reports high-signal async and cancellation failures early (53 diagnostics, many with code fixes):
- missing
CancellationTokenon public async methods and framework handlers (controllers, Minimal APIs, MediatR, SignalR,BackgroundService) - tokens accepted but not propagated to
HttpClient, EF Core,Task.Delay, and other cancellable APIs - loops and async streams that ignore cancellation (
ThrowIfCancellationRequested,.WithCancellation,[EnumeratorCancellation]) - timeout
CancellationTokenSourcethat silently drops a parent token (CreateLinkedTokenSource+CancelAfter) - sync-over-async and blocking I/O (
.Result/.Wait(),Thread.Sleep,SemaphoreSlim.Wait(), blockingFile/ stream / socket APIs,Process.WaitForExit(), blocking sync primitives) async void, unawaited fire-and-forget calls, swallowedOperationCanceledException, and resource-lifetime bugs (undisposed CTS locals and fields, prematureusingdisposal)
When the analyzer cannot prove a problem statically, it stays quiet. High-signal feedback, not noisy guesses.
Install
<PackageReference Include="CancelCop.Analyzer" Version="1.52.42">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Or:
dotnet add package CancelCop.Analyzer
Install-Package CancelCop.Analyzer -Version 1.52.42
No runtime dependency is added to your app. CancelCop runs as a Roslyn analyzer during build and in supported IDEs. Use PrivateAssets="all" so the analyzer stays a development dependency for libraries.
See it work
Product-flow diagrams from the real sample build (CC001–CC029 diagnostic text):
1. Build / IDE diagnostics (CancellationToken and async)
2. Before / after code fix (HttpClient token propagation)
3. Product loop — analyzer, code fixes, and CI
30-second path
- Reference the package with
PrivateAssets="all". - Build in the IDE or with
dotnet buildso analyzers run. - Fix any
CC00xwarnings (most have one-click code fixes). - Optionally promote critical rules to errors in
.editorconfigwhen the codebase is clean:
[*.cs]
dotnet_diagnostic.CC002.severity = error
dotnet_diagnostic.CC015.severity = error
- Keep the sample project handy for rule demos:
dotnet build samples/CancelCop.Sample
Feature snapshot
| Area | What CancelCop does |
|---|---|
| Token presence | Flags public/protected async methods and framework handlers missing CancellationToken. |
| Propagation | Requires tokens to flow into HttpClient, EF Core, and other cancellable overloads when a token is in scope. |
| ASP.NET Core | Controllers, Minimal APIs, SignalR hubs, middleware via HttpContext.RequestAborted. |
| Hosted services | BackgroundService.ExecuteAsync must observe the stopping token. |
| gRPC / MediatR | Observes ServerCallContext.CancellationToken and handler signatures. |
| Async streams | await foreach + .WithCancellation; iterators need [EnumeratorCancellation]. |
| Timeout CTS | Links parent tokens with CreateLinkedTokenSource + CancelAfter (CC029). |
| Sync-over-async | .Result / .Wait() / GetAwaiter().GetResult(), Thread.Sleep, SemaphoreSlim.Wait(), blocking file I/O. |
| Async hygiene | async void, void-returning async lambdas, swallowed cancellation, await using, CTS disposal. |
| Code fixes | Most rules offer compilable one-click fixes; Fix All is supported where safe. |
Compatibility
- Analyzer assemblies target .NET Standard 2.0 and compile against Roslyn 4.8 (Visual Studio 2022 17.8+ / .NET SDK 8+ hosts)
- Consumer projects can target any framework supported by a compatible compiler host
- ASP.NET Core, EF Core, HttpClient, gRPC, SignalR, MediatR, BackgroundService
IAsyncEnumerable<T>, ValueTask /ValueTask<T>
Analyzer Rules
| Rule | Description | Severity | Code Fix |
|---|---|---|---|
| CC001 | Public async methods must have CancellationToken parameter | Warning | ✅ |
| CC002 | CancellationToken must be propagated to async calls | Warning | ✅ |
| CC003 | EF Core queries must pass CancellationToken | Warning | ✅ |
| CC004 | HttpClient methods must pass CancellationToken | Warning | ✅ |
| CC005A | MediatR handlers must accept CancellationToken | Warning | ✅ |
| CC005B | Controller actions must accept CancellationToken | Warning | ✅ |
| CC005C | Minimal API endpoints must accept CancellationToken | Warning | ✅ |
| CC006 | CancellationToken should be the last parameter | Info | ❌ |
| CC009 | Loops should check for cancellation | Warning | ✅ |
| CC010 | await foreach should flow a CancellationToken via .WithCancellation |
Warning | ✅ |
| CC011 | Async-iterator CancellationToken should be [EnumeratorCancellation] |
Warning | ✅ |
| CC012 | Avoid passing CancellationToken.None/default when a token is in scope |
Info | ✅ |
| CC013 | Avoid Thread.Sleep in async code; use await Task.Delay |
Warning | ✅ |
| CC014 | CancellationTokenSource should be disposed |
Warning | ✅ |
| CC015 | Avoid blocking on async code (.Result/.Wait()/.GetAwaiter().GetResult()) |
Warning | ✅ |
| CC016 | CancellationToken parameter is accepted but never used |
Info | ❌ |
| CC017 | BackgroundService.ExecuteAsync should observe its stopping token |
Warning | ❌ |
| CC018 | SignalR hub methods should accept a CancellationToken |
Warning | ✅ |
| CC019 | Broad catch swallows OperationCanceledException |
Info | ✅ |
| CC020 | gRPC method should observe ServerCallContext.CancellationToken |
Warning | ❌ |
| CC021 | Method should observe HttpContext.RequestAborted |
Info | ❌ |
| CC022 | Prefer await CancelAsync() over Cancel() in async code |
Info | ✅ |
| CC023 | Avoid async void (non-event-handler) |
Warning | ✅ |
| CC024 | Avoid async lambdas converted to Action |
Warning | ❌ |
| CC025 | Prefer await using for IAsyncDisposable |
Info | ✅ |
| CC026 | Avoid SemaphoreSlim.Wait() in async code; use await WaitAsync() |
Warning | ✅ |
| CC027 | Returned task uses a disposed using resource |
Warning | ❌ |
| CC028 | Avoid blocking System.IO calls (File, StreamReader, StreamWriter, Stream) in async code; use the async counterpart |
Warning | ✅ |
| CC029 | Timeout CancellationTokenSource should link the in-scope token (CreateLinkedTokenSource + CancelAfter) |
Warning | ✅ |
| CC030 | Avoid blocking Process.WaitForExit() in async code; use await WaitForExitAsync(token) |
Warning | ✅ |
| CC031 | Avoid blocking synchronization primitives (ManualResetEventSlim.Wait, WaitHandle.WaitOne, Monitor.Wait, ReaderWriterLockSlim.Enter*Lock/TryEnter*Lock, ReaderWriterLock.Acquire*Lock/UpgradeToWriterLock, Barrier.SignalAndWait) in async code |
Warning | ❌ |
| CC032 | Async call discarded in non-async code, where the compiler's CS4014 does not fire | Warning | ❌ |
| CC033 | CancellationTokenSource field created by the type and never disposed |
Warning | ❌ |
| CC034 | ParallelOptions created without CancellationToken while a token is in scope |
Warning | ✅ |
| CC035 | Empty catch (OperationCanceledException) silently discards the cancellation |
Info | ❌ |
| CC036 | Blocking Socket call (Receive, Send, Accept, Connect, …) in async code |
Warning | ❌ |
| CC037 | Blocking TcpClient.Connect in async code |
Warning | ✅ |
| CC038 | Blocking TcpListener.AcceptTcpClient / AcceptSocket in async code |
Warning | ✅ |
| CC039 | Blocking UdpClient.Receive in async code |
Warning | ✅ |
| CC040 | Blocking HttpListener.GetContext in async code |
Warning | ✅ |
| CC041 | Blocking NamedPipeServerStream.WaitForConnection in async code |
Warning | ✅ |
| CC042 | Blocking NamedPipeClientStream.Connect in async code |
Warning | ✅ |
| CC043 | Blocking Dns.GetHostAddresses in async code |
Warning | ✅ |
| CC044 | Blocking Dns.GetHostEntry in async code |
Warning | ✅ |
| CC045 | Blocking DbConnection.Open in async code |
Warning | ✅ |
| CC046 | Blocking DbCommand.ExecuteReader in async code |
Warning | ✅ |
| CC047 | Blocking DbCommand.ExecuteNonQuery in async code |
Warning | ✅ |
| CC048 | Blocking DbCommand.ExecuteScalar in async code |
Warning | ✅ |
| CC049 | Blocking SmtpClient.Send in async code |
Warning | ✅ |
| CC050 | Blocking Ping.Send in async code |
Warning | ✅ |
| CC051 | Blocking SslStream.AuthenticateAsClient in async code |
Warning | ✅ |
| CC052 | Blocking WebRequest.GetResponse in async code |
Warning | ✅ |
| CC053 | Blocking Thread.Join in async code (no TAP counterpart on any shipped .NET — reported without a rewrite; moved out of CC031) |
Warning | ❌ |
Quick Examples
CC001: Missing CancellationToken Parameter
// ❌ Warning CC001
public async Task ProcessDataAsync()
{
await Task.Delay(100);
}
// ✅ Fixed
public async Task ProcessDataAsync(CancellationToken cancellationToken = default)
{
await Task.Delay(100, cancellationToken);
}
Convention ASP.NET middleware Invoke/InvokeAsync(HttpContext) is not flagged: the pipeline does not inject a CancellationToken parameter. Use context.RequestAborted (CC002/CC004/CC021).
CC002: Token Not Propagated
// ❌ Warning CC002 - token available but not passed
public async Task ProcessAsync(CancellationToken cancellationToken)
{
await Task.Delay(100); // Should pass cancellationToken
await DoWorkAsync(); // Should pass cancellationToken
}
// ✅ Fixed
public async Task ProcessAsync(CancellationToken cancellationToken)
{
await Task.Delay(100, cancellationToken);
await DoWorkAsync(cancellationToken);
}
When the enclosing method has an HttpContext (or gRPC ServerCallContext) instead of a token parameter, the same rule flows context.RequestAborted / context.CancellationToken:
// ❌ Warning CC002 — RequestAborted is in scope but not passed
public async Task InvokeAsync(HttpContext context)
{
await Task.Delay(100);
}
// ✅ Fixed
public async Task InvokeAsync(HttpContext context)
{
await Task.Delay(100, context.RequestAborted);
}
CC003: EF Core Without Token
// ❌ Warning CC003
public async Task<User?> GetUserAsync(int id, CancellationToken cancellationToken)
{
return await _context.Users.FirstOrDefaultAsync(u => u.Id == id);
}
// ✅ Fixed
public async Task<User?> GetUserAsync(int id, CancellationToken cancellationToken)
{
return await _context.Users.FirstOrDefaultAsync(u => u.Id == id, cancellationToken);
}
CC004: HttpClient Without Token
// ❌ Warning CC004
public async Task<string> FetchDataAsync(CancellationToken cancellationToken)
{
return await _httpClient.GetStringAsync("https://api.example.com");
}
// ✅ Fixed
public async Task<string> FetchDataAsync(CancellationToken cancellationToken)
{
return await _httpClient.GetStringAsync("https://api.example.com", cancellationToken);
}
Middleware with no token parameter is covered too — the in-scope token is RequestAborted:
// ❌ Warning CC004
public async Task InvokeAsync(HttpContext context)
{
return await _httpClient.GetStringAsync("https://api.example.com");
}
// ✅ Fixed
public async Task InvokeAsync(HttpContext context)
{
return await _httpClient.GetStringAsync("https://api.example.com", context.RequestAborted);
}
CC005B: Controller Action Without Token
// ❌ Warning CC005B
[HttpGet]
public async Task<IActionResult> GetUsers()
{
var users = await _service.GetUsersAsync();
return Ok(users);
}
// ✅ Fixed - ASP.NET Core injects the token automatically
[HttpGet]
public async Task<IActionResult> GetUsers(CancellationToken cancellationToken)
{
var users = await _service.GetUsersAsync(cancellationToken);
return Ok(users);
}
CC005C: Minimal API Without Token
// ❌ Warning CC005C
app.MapGet("/users", async () => await GetUsersAsync());
// ✅ Fixed
app.MapGet("/users", async (CancellationToken ct) => await GetUsersAsync(ct));
// ❌ Warning CC005C — method-group handlers are analysed too (v1.4.4);
// the fix adds `CancellationToken cancellationToken = default` to GetUsersAsync itself
app.MapGet("/users", GetUsersAsync);
CC006: Token Not Last Parameter
// ℹ️ Info CC006 - convention suggests token should be last
public async Task ProcessAsync(CancellationToken cancellationToken, string name)
{
}
// ✅ Better - follows .NET conventions
public async Task ProcessAsync(string name, CancellationToken cancellationToken)
{
}
CC009: Loop Without Cancellation Check
// ❌ Warning CC009 - loop doesn't check for cancellation
public async Task ProcessItemsAsync(List<Item> items, CancellationToken cancellationToken)
{
foreach (var item in items) // Could process 1M items without checking!
{
await ProcessAsync(item);
}
}
// ✅ Fixed
public async Task ProcessItemsAsync(List<Item> items, CancellationToken cancellationToken)
{
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessAsync(item);
}
}
CC010: await foreach Without a Token
// ❌ Warning CC010 - the async stream never receives the token
await foreach (var item in source)
{
}
// ✅ Fixed - .WithCancellation flows the token to the producer
await foreach (var item in source.WithCancellation(cancellationToken))
{
}
CC011: Async Iterator Token Without [EnumeratorCancellation]
// ❌ Warning CC011 - WithCancellation can't deliver a token to this parameter
public async IAsyncEnumerable<int> ReadAsync(CancellationToken token)
{
yield return await NextAsync(token);
}
// ✅ Fixed
public async IAsyncEnumerable<int> ReadAsync([EnumeratorCancellation] CancellationToken token)
{
yield return await NextAsync(token);
}
CC012: Explicit CancellationToken.None When a Token Is in Scope
// ℹ️ Info CC012 - discards cancellation even though a token is available
public async Task RunAsync(CancellationToken cancellationToken)
=> await DoAsync(CancellationToken.None);
// ✅ Fixed
public async Task RunAsync(CancellationToken cancellationToken)
=> await DoAsync(cancellationToken);
CC013: Thread.Sleep in Async Code
// ❌ Warning CC013 - blocks the thread and ignores cancellation
public async Task RunAsync(CancellationToken ct)
{
Thread.Sleep(1000);
}
// ✅ Fixed
public async Task RunAsync(CancellationToken ct)
{
await Task.Delay(1000, ct);
}
CC014: Undisposed CancellationTokenSource
// ❌ Warning CC014 - the source's timer/handle leak
var cts = new CancellationTokenSource();
await DoAsync(cts.Token);
// ✅ Fixed
using var cts = new CancellationTokenSource();
await DoAsync(cts.Token);
CC015: Blocking on Async Code
// ❌ Warning CC015 - can deadlock and discards cancellation
public async Task<int> RunAsync()
=> GetValueAsync().Result;
// ✅ Fixed
public async Task<int> RunAsync()
=> await GetValueAsync();
CC016: Unused CancellationToken Parameter
// ℹ️ Info CC016 - accepts a token but never observes it
public async Task SaveAsync(string text, CancellationToken cancellationToken)
{
await File.WriteAllTextAsync("f.txt", text); // token ignored
}
// ✅ Fixed
public async Task SaveAsync(string text, CancellationToken cancellationToken)
{
await File.WriteAllTextAsync("f.txt", text, cancellationToken);
}
CC017: BackgroundService Ignoring Its Stopping Token
// ❌ Warning CC017 - never stops on shutdown
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (true) { await DoWorkAsync(); }
}
// ✅ Fixed
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested) { await DoWorkAsync(stoppingToken); }
}
CC018: SignalR Hub Method Without a Token
// ❌ Warning CC018 - keeps running after the client disconnects
public async Task Broadcast(string message)
=> await Clients.All.SendAsync("recv", message);
// ✅ Fixed
public async Task Broadcast(string message, CancellationToken cancellationToken)
=> await Clients.All.SendAsync("recv", message, cancellationToken);
CC019: Broad catch Swallowing Cancellation
// ℹ️ Info CC019 - also swallows OperationCanceledException
try { await DoAsync(token); }
catch (Exception ex) { Log(ex); }
// ✅ Fixed - let cancellation propagate
try { await DoAsync(token); }
catch (Exception ex) when (ex is not OperationCanceledException) { Log(ex); }
CC020: gRPC Method Ignoring ServerCallContext.CancellationToken
// ❌ Warning CC020 - keeps running after the client cancels
public override async Task<Reply> Handle(Request request, ServerCallContext context)
=> new Reply { Value = await _db.LoadAsync() };
// ✅ Fixed
public override async Task<Reply> Handle(Request request, ServerCallContext context)
=> new Reply { Value = await _db.LoadAsync(context.CancellationToken) };
CC021: Method Ignoring HttpContext.RequestAborted
// ℹ️ Info CC021 - work continues after the client disconnects
public async Task InvokeAsync(HttpContext context)
=> await _service.DoWorkAsync();
// ✅ Fixed
public async Task InvokeAsync(HttpContext context)
=> await _service.DoWorkAsync(context.RequestAborted);
CC022: Prefer CancelAsync() Over Cancel()
// ℹ️ Info CC022 - runs callbacks synchronously on this thread
public async Task StopAsync(CancellationTokenSource cts)
=> cts.Cancel();
// ✅ Fixed
public async Task StopAsync(CancellationTokenSource cts)
=> await cts.CancelAsync();
CC023: async void
// ❌ Warning CC023 - cannot be awaited; exceptions crash the process
public async void ProcessAsync() => await DoWorkAsync();
// ✅ Fixed
public async Task ProcessAsync() => await DoWorkAsync();
CC024: async Lambda Converted to Action
// ❌ Warning CC024 - the async body runs fire-and-forget (async void)
Parallel.ForEach(items, async item => await ProcessAsync(item));
// ✅ Fixed - use an API that awaits, e.g.
await Parallel.ForEachAsync(items, async (item, ct) => await ProcessAsync(item, ct));
CC025: await using for IAsyncDisposable
// ℹ️ Info CC025 - Dispose() blocks on the async cleanup
using var resource = new AsyncResource();
// ✅ Fixed
await using var resource = new AsyncResource();
CC026: SemaphoreSlim.Wait() in Async Code
// ❌ Warning CC026 - blocks the thread; a classic deadlock source
public async Task RunAsync(SemaphoreSlim gate, CancellationToken ct)
{
gate.Wait();
}
// ✅ Fixed
public async Task RunAsync(SemaphoreSlim gate, CancellationToken ct)
{
await gate.WaitAsync(ct);
}
CC027: Returned Task Uses a Disposed using Resource
// ❌ Warning CC027 - the stream is disposed before the returned task completes
public Task<byte[]> ReadAsync(string path)
{
using var stream = File.OpenRead(path);
return ReadAllBytesAsync(stream);
}
// ✅ Fixed - make the method async so the resource lives until completion
public async Task<byte[]> ReadAsync(string path)
{
using var stream = File.OpenRead(path);
return await ReadAllBytesAsync(stream);
}
CC028: Blocking I/O in Async Code
// ❌ Warning CC028 - blocks the thread for the whole disk read
public async Task<string> LoadAsync(string path)
{
var text = File.ReadAllText(path); // also flags StreamReader.ReadToEnd()/ReadLine() and StreamWriter.Write/WriteLine/Flush
await Task.Yield();
return text;
}
// ✅ Fixed - the async counterpart yields the thread and accepts a CancellationToken
public async Task<string> LoadAsync(string path, CancellationToken cancellationToken)
{
var text = await File.ReadAllTextAsync(path, cancellationToken);
return text;
}
// ❌ Warning CC028 - the Stream primitives block too, on any Stream subclass
public async Task ArchiveAsync(Stream source, Stream destination)
{
source.CopyTo(destination); // also flags Stream Read/Write/Flush
await Task.Yield();
}
// ✅ Fixed
public async Task ArchiveAsync(Stream source, Stream destination, CancellationToken cancellationToken)
{
await source.CopyToAsync(destination, cancellationToken);
}
MemoryStreamis excluded — it is backed by an in-memory buffer, so the "blocking" call never leaves the CPU and the async form only wraps the same synchronous work.
CC029: Timeout CTS Should Link the In-Scope Token
// ❌ Warning CC029 - timeout ignores the caller's cancellation (e.g. RequestAborted)
public async Task RunAsync(CancellationToken cancellationToken)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await DoAsync(cts.Token);
}
// ✅ Fixed - parent cancel and timeout both apply
public async Task RunAsync(CancellationToken cancellationToken)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(30));
await DoAsync(cts.Token);
}
CC030: Blocking Process.WaitForExit() in Async Code
// ❌ Warning CC030 - blocks a thread for an unbounded wait on an external process
public async Task RunToolAsync(Process process)
{
process.WaitForExit();
await Task.Yield();
}
// ✅ Fixed - yields the thread and honours cancellation
public async Task RunToolAsync(Process process, CancellationToken cancellationToken)
{
await process.WaitForExitAsync(cancellationToken);
}
The
WaitForExit(int)timeout overload is not flagged: it returnsboolandWaitForExitAsynctakes only a token, so there is no rewrite that preserves the call's meaning.
CC031: Blocking Synchronization Primitives in Async Code
// ❌ Warning CC031 - parks a pooled thread until another thread signals
public async Task WaitForReadyAsync(ManualResetEventSlim ready)
{
ready.Wait();
await Task.Yield();
}
// ✅ Fixed - an awaitable signal yields the thread and honours cancellation
public async Task WaitForReadyAsync(SemaphoreSlim ready, CancellationToken cancellationToken)
{
await ready.WaitAsync(cancellationToken);
}
Analyzer-only by design. These primitives have no
…Asynccounterpart in .NET, so resolving the finding is a design change — aSemaphoreSlim, aTaskCompletionSource, or awaiting the task instead of joining the thread — rather than a mechanical rewrite.SemaphoreSlim.Waitbelongs to CC026, which can offer a real fix.Thread.Joinbelonged here until v1.52.42 and now has its own dedicated rule, CC053.ReaderWriterLockSlim.Enter*Lock/TryEnter*Lock,ReaderWriterLock.Acquire*Lock, andBarrier.SignalAndWaitare included because they are notWaitHandlemembers and would otherwise be a silent false negative. A zero-timeoutTryEnterorAcquire*Lockis an immediate probe and stays quiet.UpgradeToWriterLock(0)still reports: a failed upgrade restores the read lock withTimeout.Infinite.Barrier.SignalAndWait(0)still reports: the last arriver runs the post-phase action before returning.
CC032: Async Call Not Awaited in Non-Async Code
// ❌ Warning CC032 - a constructor cannot be async, so CS4014 never fires here
public Service()
{
InitializeAsync();
}
// ✅ Fixed - the caller awaits, so cancellation and failures flow
public async Task StartAsync(CancellationToken cancellationToken)
{
await InitializeAsync(cancellationToken);
}
Fills a real compiler gap: CS4014 only fires inside an async method. In a constructor, a synchronous method, or a non-async lambda the compiler says nothing. A task that is assigned, returned, passed as an argument, or explicitly discarded with
_ =is not dropped and is not flagged —_ =is the documented way to opt in deliberately. Analyzer-only: the right resolution depends on intent.
CC033: CancellationTokenSource Field Never Disposed
// ❌ Warning CC033 - created by this type, never disposed
public class Worker
{
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
}
// ✅ Fixed - the owner disposes what it created
public sealed class Worker : IDisposable
{
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public void Dispose() => _cts.Dispose();
}
Complements CC014, which covers local sources and can offer a
usingfix. A field's lifetime is the object's, so the resolution is a design change and CC033 is analyzer-only. It fires only when the declaring type creates the source — an injected one is owned by whoever created it, and disposing it would be a bug. Fields that escape (returned or passed as an argument) andstaticfields stay quiet.
CC034: ParallelOptions Missing a CancellationToken
// ❌ Warning CC034 - nothing can stop this loop
public void Process(int[] items, CancellationToken cancellationToken)
{
var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
Parallel.ForEach(items, options, Handle);
}
// ✅ Fixed - the loop observes cancellation between partitions
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 4,
CancellationToken = cancellationToken,
};
ParallelOptions.CancellationTokenis the only way to cancel aParallelloop. CC002 cannot see this: it matches calls with token-accepting overloads, but here the token is a property in an object initializer andParallel.ForEachhas no token-taking overload at all. Fires only when a token is actually in scope, and stays quiet when the token is assigned afterwards (options.CancellationToken = token).
CC035: Cancellation Silently Swallowed by an Empty Catch
// ❌ Info CC035 - the caller cannot tell the save did not happen
try
{
await SaveAsync(cancellationToken);
}
catch (OperationCanceledException)
{
}
CC019 covers a broad catch that swallows cancellation among everything else; a clause naming
OperationCanceledExceptionexplicitly is outside its scope. Scoped to the empty body: any statement, awhenfilter, a rethrow, or even a comment recording the intent means the author considered the case, and the rule stays quiet. Socatch (TaskCanceledException) { /* expected on shutdown */ }— the idiomatic wait-until-cancelled — is clean.
CC036: Blocking Socket Calls in Async Code
// ❌ Warning CC036 - can block indefinitely waiting for a connection
public async Task ServeAsync(Socket listener)
{
var client = listener.Accept();
}
// ✅ Fixed
var client = await listener.AcceptAsync(cancellationToken);
CC028 already covers every
Stream, so aNetworkStreamis handled there.Socketitself is not, because its async counterparts are not signature-compatible —Receive(byte[])pairs withReceiveAsync(Memory<byte>, CancellationToken)— and that compatibility is exactly what makes CC028's rewrites safe. Loosening it would trade fix safety for reach, so this is a separate, analyzer-only rule.
CC037: Blocking TcpClient.Connect in Async Code
// ❌ Warning CC037 - parks a pool thread until the handshake finishes
public async Task RunAsync(TcpClient client, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
client.Connect(host, port);
}
// ✅ Fixed (.NET 6+; older targets use ConnectAsync(host, port) without a token)
await client.ConnectAsync(host, port, cancellationToken);
CC036 covers
Socket.Connect. Application code almost always uses theTcpClientwrapper, which none of the previous rules reported.client.Client.Blocking = falsesilences only IP/endpointConnecton that same simple local, parameter, or field (the call returnsWouldBlockinstead of parking), including in top-level programs. HostnameConnectstill reports —TcpClient.Connect(string, int)does synchronous DNS. Property or method receivers are not exempt (a getter may return a new instance). An unrelatedSocket.Blocking = falsedoes not exempt, and reassigning the client afterBlocking = falseinvalidates the exemption. The fixer rewrites a safeConnecttoawait ConnectAsync, flowing an in-scope token. Namedhostname:arguments are reported without a rewrite (ConnectAsyncuseshost). Null-conditional calls, positions whereawaitcannot compile, and a this/base/this-alias call insideConnectAsyncare reported without a fix. The token-takingConnectAsyncoverload is modern .NET only —netstandard2.0/ .NET Framework have the tokenless form.
CC038: Blocking TcpListener Accept in Async Code
// ❌ Warning CC038 - parks a pool thread until a client connects
public async Task RunAsync(TcpListener listener, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
listener.AcceptTcpClient();
}
// ✅ Fixed (.NET 6+; older targets use AcceptTcpClientAsync() without a token)
await listener.AcceptTcpClientAsync(cancellationToken);
CC036 covers
Socket.Accept. CC037 coversTcpClient.Connect. The listener accept path is a third type, which none of the previous rules reported. A positiveif (listener.Pending())/while (Pending())/while (flag && Pending())guard, the inverted poll (if (!Pending()) continue;then accept), andlistener.Server.Blocking = falsestay quiet;if (!listener.Pending()) Acceptis the blocking path and still reports. The fixer rewrites a safe accept toawait AcceptTcpClientAsync/await AcceptSocketAsync, flowing an in-scope token when the rewritten call binds. Null-conditional calls, positions whereawaitcannot compile, and a this/base/this-alias call inside the matchingAccept*Asyncare reported without a rewrite. Unusable TAP hiders stay quiet. The token-takingAccept*Asyncoverloads are modern .NET only —netstandard2.0/ .NET Framework have the tokenless form.
CC039: Blocking UdpClient.Receive in Async Code
// ❌ Warning CC039 - parks a pool thread until a datagram arrives
public async Task RunAsync(UdpClient client, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
IPEndPoint? remote = null;
client.Receive(ref remote);
}
// ✅ Fixed (.NET 6+; older targets use ReceiveAsync() without a token)
await client.ReceiveAsync(cancellationToken);
CC036 covers
Socket.Receive. CC037 coversTcpClient.Connect. CC038 coversTcpListeneraccept. The UDP wrapper is a fourth type, which none of the previous rules reported.if (client.Available > 0),while (Available > 0), the inverted poll (if (Available == 0) continue;then receive), andclient.Client.Blocking = falsestay quiet;if (Available == 0) Receiveis the blocking path and still reports. The fixer rewrites a discardedReceive(ref endpoint)statement tovar received = await ReceiveAsync(...)and assignsendpoint = received.RemoteEndPoint.ReceiveAsyncreturnsUdpReceiveResultand does not take therefendpoint, so a value-use of thebyte[]is reported without a rewrite. A bracelessif/whilebody, null-conditional calls, await-illegal positions, and a this/base/this-alias call insideReceiveAsyncare reported without a rewrite. Unusable TAP hiders stay quiet. The token-takingReceiveAsyncoverload is modern .NET only —netstandard2.0/ .NET Framework have the tokenless form.
CC040: Blocking HttpListener.GetContext in Async Code
// ❌ Warning CC040 - parks a pool thread until a request arrives
public async Task RunAsync(HttpListener listener, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
listener.GetContext();
}
// ✅ Fixed
await listener.GetContextAsync();
CC036–CC039 cover Socket / TcpClient / TcpListener / UdpClient. The HTTP listener is a fifth type, which none of the previous rules reported. The fixer rewrites a safe
GetContext()toawait GetContextAsync().GetContextAsyncdoes not take aCancellationToken, so the rewrite never invents one. Null-conditional calls and positions whereawaitcannot compile are reported without a rewrite.HttpListeneris sealed.
CC041: Blocking NamedPipeServerStream.WaitForConnection in Async Code
// ❌ Warning CC041 - parks a pool thread until a client connects
public async Task RunAsync(NamedPipeServerStream server, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
server.WaitForConnection();
}
// ✅ Fixed
await server.WaitForConnectionAsync(cancellationToken);
CC028 covers File/Stream
Read/Write/CopyTo/Flush. CC036–CC040 cover Socket / TcpClient / TcpListener / UdpClient / HttpListener. The named-pipe server is a sixth type, which none of the previous rules reported. The fixer rewrites a safeWaitForConnection()toawait WaitForConnectionAsync, flowing an in-scope token when the rewritten call still binds. Null-conditional calls and positions whereawaitcannot compile are reported without a rewrite.NamedPipeServerStreamis sealed. The token-takingWaitForConnectionAsyncoverload is modern .NET only — .NET Framework has the tokenless form.
CC042: Blocking NamedPipeClientStream.Connect in Async Code
// ❌ Warning CC042 - parks a pool thread until the server accepts
public async Task RunAsync(NamedPipeClientStream client, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
client.Connect();
}
// ✅ Fixed
await client.ConnectAsync(cancellationToken);
CC041 covers
NamedPipeServerStream.WaitForConnection. The client connect is a sibling type, which none of the previous rules reported. TheintandTimeSpantimeout overloads still park the thread and also report. The fixer rewrites a safeConnecttoawait ConnectAsync, keeping the timeout argument and flowing an in-scope token when the rewritten call still binds. There is no tokenlessConnectAsync(TimeSpan), so that overload is reported without a rewrite unless a token is in scope. Null-conditional calls and positions whereawaitcannot compile are reported without a rewrite.NamedPipeClientStreamis sealed.ConnectAsyncis modern .NET only — the rule stays quiet where that member is absent (.NET Framework has noConnectAsyncat all).
CC043: Blocking Dns.GetHostAddresses in Async Code
// ❌ Warning CC043 - parks a pool thread on a DNS query
public async Task RunAsync(string host, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Dns.GetHostAddresses(host);
}
// ✅ Fixed
await Dns.GetHostAddressesAsync(host, cancellationToken);
CC036–CC042 cover Socket / Tcp / Udp / HttpListener / named-pipe. DNS is a separate type, which none of the previous rules reported. CC002 cannot see it (no token overload of the invoked method). The
AddressFamilyoverload andusing static System.Net.Dnsalso report. A compile-time constant IP ("127.0.0.1","::1",const string) is a parse, not a query, and stays quiet;"localhost"and non-const locals still report. The fixer rewrites a safeGetHostAddressestoawait GetHostAddressesAsync, flowing an in-scope token when the rewritten call still binds. TheAddressFamilyTAP has an optional token, so a tokenless rewrite still compiles. Positions whereawaitcannot compile are reported without a rewrite. Ausing staticidentifier rewrite is withheld when a same-named helper would capture the bind.Dnsis a static type. The token-takingGetHostAddressesAsyncoverload is modern .NET only — .NET Framework has the tokenless form.
CC044: Blocking Dns.GetHostEntry in Async Code
// ❌ Warning CC044 - parks a pool thread on a DNS query (incl. reverse lookup)
public async Task RunAsync(string host, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Dns.GetHostEntry(host);
}
// ✅ Fixed
await Dns.GetHostEntryAsync(host, cancellationToken);
CC043 covers
GetHostAddressesonly. GetHostEntry is a sibling, which none of the previous rules reported. A numeric IP still reports — unlike GetHostAddresses, GetHostEntry does reverse DNS for that address. TheAddressFamilyandIPAddressoverloads andusing staticalso report. The fixer rewrites a safeGetHostEntrytoawait GetHostEntryAsync, flowing an in-scope token when the rewritten call still binds toSystem.Net.Dns. TheIPAddressTAP is tokenless, so that rewrite never invents a token. TheAddressFamilyTAP has an optional token. Ausing staticidentifier rewrite is withheld when a same-named helper would capture the bind. Positions whereawaitcannot compile are reported without a rewrite. The token-taking stringGetHostEntryAsyncoverload is modern .NET only; theIPAddressasync form is tokenless.
CC045: Blocking DbConnection.Open in Async Code
// ❌ Warning CC045 - parks a pool thread on a database handshake
public async Task RunAsync(DbConnection connection, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
connection.Open();
}
// ✅ Fixed
await connection.OpenAsync(cancellationToken);
CC003 covers EF Core queries. ADO.NET
Openis a separate type, which none of the previous rules reported. Concrete providers match through the override chain.DbCommand.ExecuteReaderis CC046. The fixer rewrites a safeOpen()toawait OpenAsync, flowing an in-scope token. Null-conditional calls and positions whereawaitcannot compile are reported without a fix.OpenAsynchas accepted aCancellationTokensince .NET Framework 4.5.
CC046: Blocking DbCommand.ExecuteReader in Async Code
// ❌ Warning CC046 - parks a pool thread on a database query
public async Task RunAsync(DbCommand command, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
command.ExecuteReader();
}
// ✅ Fixed
await command.ExecuteReaderAsync(cancellationToken);
CC003 covers EF Core queries. CC045 covers
DbConnection.Open. ADO.NETExecuteReaderis a separate member, which none of the previous rules reported. The method is not virtual — providers hide it withnewfor a covariant reader — and those hiders still report when they match the framework shape. Custom helpers, generic helpers, and statics stay quiet.IDbCommandstays quiet.ExecuteNonQueryis CC047.ExecuteScalaris CC048. The fixer rewrites a safeExecuteReadertoawait ExecuteReaderAsync, preserving aCommandBehaviorargument and flowing an in-scope token. When the original call is a receiver or is followed by!, the await is parenthesized. Null-conditional calls and positions whereawaitcannot compile are reported without a fix. ProvidernewTAP hiders still match —ExecuteReaderAsync()/ExecuteReaderAsync(CancellationToken)are not virtual. ATask<int>hider is not a reader API and stays quiet.ExecuteReaderAsynchas accepted aCancellationTokensince .NET Framework 4.5.
CC047: Blocking DbCommand.ExecuteNonQuery in Async Code
// ❌ Warning CC047 - parks a pool thread on a command that does not return rows
public async Task RunAsync(DbCommand command, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
command.ExecuteNonQuery();
}
// ✅ Fixed
await command.ExecuteNonQueryAsync(cancellationToken);
CC003 covers EF Core queries. CC045 covers
DbConnection.Open. CC046 coversExecuteReader. ADO.NETExecuteNonQueryis a separate member, which none of the previous rules reported. Overrides andnewhiders that match the framework shape still report. Custom helpers, generic helpers, andIDbCommandstay quiet.ExecuteScalaris CC048. The fixer rewrites a safeExecuteNonQuery()toawait ExecuteNonQueryAsync, flowing an in-scope token. Null-conditional calls and positions whereawaitcannot compile are reported without a fix.ExecuteNonQueryAsynchas accepted aCancellationTokensince .NET Framework 4.5.ExecuteNonQueryAsynchas accepted aCancellationTokensince .NET Framework 4.5.
CC048: Blocking DbCommand.ExecuteScalar in Async Code
// ❌ Warning CC048 - parks a pool thread on a single-value query
public async Task RunAsync(DbCommand command, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
command.ExecuteScalar();
}
// ✅ Fixed
await command.ExecuteScalarAsync(cancellationToken);
CC003 covers EF Core queries. CC045 covers
DbConnection.Open. CC046 coversExecuteReader. CC047 coversExecuteNonQuery. ADO.NETExecuteScalaris a separate member, which none of the previous rules reported. Overrides andnewhiders that match the framework shape still report, including a more-derived return such asstring. Custom helpers, generic helpers, statics,voidhiders,Task/ValueTaskhiders, andIDbCommandstay quiet. The fixer rewrites a safeExecuteScalar()toawait ExecuteScalarAsync, flowing an in-scope token. Null-conditional calls, positions whereawaitcannot compile, and a this/base/this-alias call insideExecuteScalarAsyncare reported without a fix. CovariantTask<string>hiders still match.ExecuteScalarAsynchas accepted aCancellationTokensince .NET Framework 4.5.
CC049: Blocking SmtpClient.Send in Async Code
// ❌ Warning CC049 - parks a pool thread on an SMTP handshake
public async Task RunAsync(SmtpClient client, MailMessage message, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
client.Send(message);
}
// ✅ Fixed
await client.SendMailAsync(message, cancellationToken);
CC004 covers HttpClient. ADO.NET rules cover database waits.
SmtpClient.Sendis a separate type, which none of the previous rules reported. The TAP counterpart isSendMailAsync, not the event-basedSendAsync. Token-takingSendMailAsyncis .NET 5+; .NET Framework has the tokenless form.Sendis not virtual;newhiders that match the framework shape still report. The fixer rewrites a safeSendtoawait SendMailAsync, flowing an in-scope token. Null-conditional calls, positions whereawaitcannot compile, and a this/base/this-alias call insideSendMailAsyncare reported without a fix.
CC050: Blocking Ping.Send in Async Code
// ❌ Warning CC050 - parks a pool thread waiting for an echo reply
public async Task RunAsync(Ping ping, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ping.Send("example.org");
}
// ✅ Fixed
await ping.SendPingAsync("example.org");
CC036–CC044 cover the Socket/Tcp/Udp/HttpListener/named-pipe/DNS families.
Ping.Sendis a separate type, which none of the previous rules reported. The TAP counterpart isSendPingAsync, not the event-basedSendAsync. The token-takingSendPingAsyncoverloads are modern .NET only and exist solely on theTimeSpanarity (host, timeout, buffer, options, token), so a bareSend(host)rewrites tokenless — appending a token argument would not bind — while the full-arity call flows the in-scope token. Null-conditional statements hoist to anis not nullguard; lock bodies, unsafe contexts, and a bareSendinside aSendPingAsyncmember are reported without a fix.
CC051: Blocking SslStream.AuthenticateAsClient in Async Code
// ❌ Warning CC051 - parks a pool thread for the entire TLS handshake
public async Task RunAsync(SslStream stream, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
stream.AuthenticateAsClient("example.org");
}
// ✅ Fixed
await stream.AuthenticateAsClientAsync("example.org");
SslStream.AuthenticateAsClientis a separate type, which none of the previous blocking rules reported. The TAP counterpart isAuthenticateAsClientAsync; only theSslClientAuthenticationOptionsarity accepts a token, so string-arity calls rewrite tokenless — appending a token argument would not bind — while an options-arity call flows the in-scope token. Null-conditional statements hoist to anis not nullguard; lock bodies, unsafe contexts, and a bareAuthenticateAsClientinside anAuthenticateAsClientAsyncmember are reported without a fix.
CC052: Blocking WebRequest.GetResponse in Async Code
// ❌ Warning CC052 - parks a pool thread for the whole request/response round trip
public async Task RunAsync(WebRequest request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
request.GetResponse();
}
// ✅ Fixed
await request.GetResponseAsync();
WebRequest.GetResponseis the legacy HTTP stack, which none of the previous blocking rules reported. The TAP counterpart isGetResponseAsync; it is parameterless and no arity in the family accepts a token, so the rewrite is always tokenless — appending a token argument would not bind — and real cancellation means moving off the legacy stack (e.g. toHttpClient). Null-conditional statements hoist to anis not nullguard; lock bodies, unsafe contexts, and a bareGetResponseinside aGetResponseAsyncmember are reported without a fix.
CC053: Blocking Thread.Join in Async Code
// ❌ Warning CC053 - parks the calling pool thread until the joined thread terminates
public async Task RunAsync(Thread thread, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
thread.Join();
}
// ✅ Preferred
await workTask; // await the task that represents the work instead of joining a raw thread
System.Threading.Threaddeclares onlyJoin(),Join(int), andJoin(TimeSpan)on current .NET (verified against the net9/net10 reference packs) and no TAPJoinAsynccounterpart on any shipped version, so CC053 is analyzer-only by design: every call is reported without a rewrite — do not expect a code fix. Prefer awaiting the task that represents the work; a blocking join in async code is a deadlock risk under a starving pool.Thread.Joinmoved out of CC031 (v1.52.42) into this dedicated, symbol-gated rule for the type itself, so each join call reports exactly once.Threadis also sealed, so no derived-type receiver shapes exist. The CC031 quick-example note above no longer listsThread.Joinfor the same reason.
Configuration
All rules are enabled by default. Configure severity in .editorconfig:
[*.cs]
# Disable a rule
dotnet_diagnostic.CC001.severity = none
# Make a rule an error (fails build)
dotnet_diagnostic.CC002.severity = error
# Make CC006 more prominent
dotnet_diagnostic.CC006.severity = warning
Compatibility and Supported Frameworks
- Analyzer assemblies target .NET Standard 2.0 and compile against Roslyn 4.8, compatible with Visual Studio 2022 17.8+ and .NET SDK 8+ compiler hosts
- Consumer projects can target any framework supported by a compatible compiler host
- ASP.NET Core (Controllers, Minimal APIs, SignalR hubs, middleware via
HttpContext.RequestAborted) - Hosted services (
BackgroundService.ExecuteAsync) - gRPC (
ServerCallContext.CancellationToken) - Entity Framework Core (curated cancellable query and save methods)
- HttpClient (curated cancellable request and content methods)
- MediatR (IRequestHandler implementations)
- Async streams (
IAsyncEnumerable<T>,[EnumeratorCancellation]) - ValueTask and ValueTask<T> return types
Project Quality
- 1,500+ regression tests with comprehensive coverage, plus a cross-analyzer false-positive guard that runs every analyzer over idiomatic code (core, framework, nested-scope, exotic-syntax) and asserts zero diagnostics
- Test-Driven Development approach
- Built on official Microsoft Roslyn APIs
- Follows .NET Analyzer best practices (every rule documented, release-tracked, and covered by
RuleCatalogTestsdrift guards)
Building from Source
# Clone the repository
git clone https://github.com/georgepwall1991/CancelCop.Analyzer.git
cd CancelCop.Analyzer
# Restore and build
dotnet restore
dotnet build
# Run tests
dotnet test
# Pack NuGet package
dotnet pack src/CancelCop.Analyzer.Package/CancelCop.Analyzer.Package.csproj -c Release
Project Structure
CancelCop.Analyzer/
├── src/
│ ├── CancelCop.Analyzer/ # Diagnostic analyzers
│ ├── CancelCop.Analyzer.CodeFixes/ # Code-fix providers
│ └── CancelCop.Analyzer.Package/ # NuGet packaging
├── tests/
│ └── CancelCop.Analyzer.Tests/ # xUnit regression suite
├── samples/
│ └── CancelCop.Sample/ # Example project with all rules
├── .github/workflows/ # CI/CD (build, test, publish)
└── docs/ # Additional documentation
Sample Project
The samples/CancelCop.Sample project demonstrates the analyzer rules with:
- focused examples grouped by diagnostic family;
- Both violation examples (triggering warnings) and correct patterns
- Detailed comments explaining why each rule matters
Build the sample to see the analyzers in action:
dotnet build samples/CancelCop.Sample
Contributing
Contributions are welcome! Please see the contribution guidelines.
Key points:
- Follow TDD approach (tests first)
- Ensure all tests pass
- Update documentation for new features
- One feature per pull request
Roadmap
CancelCop now ships 53 rules spanning token presence, propagation, positioning, loop checks,
lifecycle, async hygiene, and framework cancellation sources. The features originally planned here have shipped (under their final IDs):
CancellationToken.None misuse → CC012, unused token parameters → CC016, async void →
CC023. New rules are added opportunistically as common cancellation pitfalls surface; bug fixes
and false-positive hardening continue each release.
License
Author
George Wall - GitHub
⭐ If CancelCop helps you write better async code, consider giving it a star!
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on CancelCop.Analyzer:
| Repository | Stars |
|---|---|
|
VahidN/DNTCommon.Web.Core
DNTCommon.Web.Core provides common scenarios' solutions for ASP.NET Core applications.
|
| Version | Downloads | Last Updated |
|---|---|---|
| 1.52.51 | 75 | 8/26/2026 |
| 1.52.50 | 63 | 8/26/2026 |
| 1.52.49 | 68 | 8/26/2026 |
| 1.52.48 | 61 | 8/26/2026 |
| 1.52.47 | 70 | 8/26/2026 |
| 1.52.46 | 78 | 8/26/2026 |
| 1.52.45 | 70 | 8/26/2026 |
| 1.52.44 | 66 | 8/26/2026 |
| 1.52.43 | 66 | 8/25/2026 |
| 1.52.42 | 70 | 8/25/2026 |
| 1.52.41 | 74 | 8/25/2026 |
| 1.52.40 | 69 | 8/25/2026 |
| 1.52.39 | 68 | 8/25/2026 |
| 1.52.38 | 76 | 8/25/2026 |
| 1.52.37 | 75 | 8/25/2026 |
| 1.52.36 | 72 | 8/25/2026 |
| 1.52.35 | 76 | 8/25/2026 |
| 1.52.34 | 77 | 8/25/2026 |
| 1.52.33 | 71 | 8/25/2026 |
| 1.52.32 | 71 | 8/25/2026 |
CC053 reports blocking Thread.Join in async code. Verified against the net9/net10 ref packs: Thread declares no TAP JoinAsync counterpart, so the rule is analyzer-only by design and reports without a rewrite. 53 diagnostics.