georgepwall1991/CancelCop.Analyzer
GitHub: georgepwall1991/CancelCop.Analyzer
一款基于 Roslyn 的 C#/.NET 编译时静态分析器,用于在开发和 CI 阶段检测并修复 CancellationToken 传递缺失与 async/await 阻塞调用等异步代码问题。
Stars: 4 | Forks: 0
# CancelCop.Analyzer
**用于 C#/.NET 的编译时 CancellationToken 和 async/await Roslyn 分析器** — 捕获缺失的 cancellation 传递、被忽略的 ASP.NET Core `RequestAborted`、EF Core 和 HttpClient 的 token 遗漏、sync-over-async 死锁、阻塞式 I/O、`async void` 以及资源生命周期 bug,让它们在编辑器和 CI 中就暴露出来,而不是等到运行时。
[](https://www.nuget.org/packages/CancelCop.Analyzer/)
[](https://www.nuget.org/packages/CancelCop.Analyzer/)
[](https://github.com/georgepwall1991/CancelCop.Analyzer/actions/workflows/ci.yml)
[](https://github.com/georgepwall1991/CancelCop.Analyzer/blob/main/LICENSE)
别再发布无法取消的异步代码了。
## 问题所在
`CancellationToken` 和正确的 async/await 用法对于响应式 .NET 应用至关重要,但 cancellation bug 往往隐藏在 API 边界中。一个没有 token 的公共方法、忽略了调用方 token 的 `HttpClient` 或 EF Core 调用、永远接收不到 `RequestAborted` 的 controller、丢弃了父级 token 的超时 CTS,或者是位于异步代码中的 `.Result` / `Thread.Sleep`,通常都能顺利通过编译,而只有在高负载、关闭进程或客户端断开连接时才会出现问题。
运行时代码审查和偶尔触发的 CA 规则往往会遗漏那些专门的 cancellation 和 async 分析器可以从你的调用点中确凿发现的问题。
## 它能捕获什么
CancelCop 能尽早报告高价值的异步和 cancellation 失败(35 项诊断,其中许多附带代码修复):
- 公共异步方法和框架处理程序(controllers、Minimal APIs、MediatR、SignalR、`BackgroundService`)缺失 `CancellationToken`
- 接收了 token 但未将其传递给 `HttpClient`、EF Core、`Task.Delay` 及其他可取消 API 的情况
- 忽略 cancellation 的循环和异步流(`ThrowIfCancellationRequested`、`.WithCancellation`、`[EnumeratorCancellation]`)
- 静默丢弃父级 token 的超时 `CancellationTokenSource`(`CreateLinkedTokenSource` + `CancelAfter`)
- sync-over-async 和阻塞式 I/O(`.Result` / `.Wait()`、`Thread.Sleep`、`SemaphoreSlim.Wait()`、阻塞式 `File` / 流 API、`Process.WaitForExit()`、阻塞式同步原语)
- `async void`、未等待的 fire-and-forget 调用、被吞掉的 `OperationCanceledException` 以及资源生命周期 bug(未释放的 CTS 局部变量和字段、过早的 `using` 释放)
当分析器无法在静态分析下确证问题时,它会**保持沉默**。提供高价值反馈,而非嘈杂的猜测。
## 安装
```
all
runtime; build; native; contentfiles; analyzers
```
或者:
```
dotnet add package CancelCop.Analyzer
```
```
Install-Package CancelCop.Analyzer -Version 1.37.0
```
不会为你的应用添加**任何运行时依赖**。CancelCop 作为 Roslyn 分析器在构建期间和受支持的 IDE 中运行。请使用 `PrivateAssets="all"`,以便对于类库项目,该分析器仅作为开发依赖项保留。
## 看看它的效果
来自真实示例构建的产品流程图(CC001–CC029 诊断文本):
### 1. 构建 / IDE 诊断(CancellationToken 和 async)

### 2. 代码修复前 / 后(HttpClient token 传递)

### 3. 产品循环 — 分析器、代码修复和 CI

## 30 秒入门
1. 使用 `PrivateAssets="all"` 引用该包。
2. 在 IDE 中或通过 `dotnet build` 进行构建,以便运行分析器。
3. 修复任何 `CC00x` 警告(大多数都有一键代码修复)。
4. 当代码库变得整洁时,可选择在 `.editorconfig` 中将关键规则提升为错误:
```
[*.cs]
dotnet_diagnostic.CC002.severity = error
dotnet_diagnostic.CC015.severity = error
```
5. 保留示例项目以方便进行规则演示:
```
dotnet build samples/CancelCop.Sample
```
## 功能概览
| 领域 | CancelCop 的作用 |
|------|---------------------|
| Token 存在性 | 标记缺失 `CancellationToken` 的公共/受保护的异步方法和框架处理程序。 |
| 传递 | 当作用域内存在 token 时,要求将 token 传递给 HttpClient、EF Core 及其他可取消的重载方法。 |
| ASP.NET Core | Controllers、Minimal APIs、SignalR hubs、通过 `HttpContext.RequestAborted` 的 middleware。 |
| 托管服务 | `BackgroundService.ExecuteAsync` 必须监听停止 token。 |
| gRPC / MediatR | 监听 `ServerCallContext.CancellationToken` 和处理程序签名。 |
| 异步流 | `await foreach` + `.WithCancellation`;迭代器需要 `[EnumeratorCancellation]`。 |
| 超时 CTS | 通过 `CreateLinkedTokenSource` + `CancelAfter` 链接父级 token(CC029)。 |
| Sync-over-async | `.Result` / `.Wait()` / `GetAwaiter().GetResult()`、`Thread.Sleep`、`SemaphoreSlim.Wait()`、阻塞式文件 I/O。 |
| Async 规范 | `async void`、返回 void 的异步 lambda、被吞掉的 cancellation、`await using`、CTS 释放。 |
| 代码修复 | 大多数规则提供可编译的一键修复;在安全的情况下支持“全部修复”。 |
## 兼容性
- 分析器程序集的目标框架是 **.NET Standard 2.0**,并基于 **Roslyn 4.8** 编译(Visual Studio 2022 17.8+ / .NET SDK 8+ 宿主)
- 使用者项目可以是兼容编译器宿主所支持的任何目标框架
- **ASP.NET Core**、**EF Core**、**HttpClient**、**gRPC**、**SignalR**、**MediatR**、**BackgroundService**
- **`IAsyncEnumerable
`**、**ValueTask** / **`ValueTask`**
## 分析器规则
| 规则 | 描述 | 严重程度 | 代码修复 |
|------|-------------|----------|----------|
| **CC001** | 公共异步方法必须具有 CancellationToken 参数 | Warning | ✅ |
| **CC002** | CancellationToken 必须被传递到异步调用中 | Warning | ✅ |
| **CC003** | EF Core 查询必须传递 CancellationToken | Warning | ✅ |
| **CC004** | HttpClient 方法必须传递 CancellationToken | Warning | ✅ |
| **CC005A** | MediatR 处理程序必须接受 CancellationToken | Warning | ✅ |
| **CC005B** | Controller 操作必须接受 CancellationToken | Warning | ✅ |
| **CC005C** | Minimal API endpoint 必须接受 CancellationToken | Warning | ✅ |
| **CC006** | CancellationToken 应该是最后一个参数 | Info | ❌ |
| **CC009** | 循环应检查 cancellation | Warning | ✅ |
| **CC010** | `await foreach` 应该通过 `.WithCancellation` 传递 CancellationToken | Warning | ✅ |
| **CC011** | Async-iterator 的 CancellationToken 应标记为 `[EnumeratorCancellation]` | Warning | ✅ |
| **CC012** | 当作用域内存在 token 时,避免传递 `CancellationToken.None`/`default` | Info | ✅ |
| **CC013** | 避免在异步代码中使用 `Thread.Sleep`;请使用 `await Task.Delay` | Warning | ✅ |
| **CC014** | `CancellationTokenSource` 应被释放 | Warning | ✅ |
| **CC015** | 避免阻塞异步代码(`.Result`/`.Wait()`/`.GetAwaiter().GetResult()`) | Warning | ✅ |
| **CC016** | 接受了 `CancellationToken` 参数但从未使用 | Info | ❌ |
| **CC017** | `BackgroundService.ExecuteAsync` 应监听其停止 token | Warning | ❌ |
| **CC018** | SignalR hub 方法应接受 `CancellationToken` | Warning | ✅ |
| **CC019** | 宽泛的 `catch` 吞掉了 `OperationCanceledException` | Info | ✅ |
| **CC020** | gRPC 方法应监听 `ServerCallContext.CancellationToken` | Warning | ❌ |
| **CC021** | 方法应监听 `HttpContext.RequestAborted` | Info | ❌ |
| **CC022** | 在异步代码中,首选 `await CancelAsync()` 而不是 `Cancel()` | Info | ✅ |
| **CC023** | 避免 `async void`(非事件处理程序) | Warning | ✅ |
| **CC024** | 避免将 `async` lambda 转换为 `Action` | Warning | ❌ |
| **CC025** | 对 `IAsyncDisposable` 首选 `await using` | Info | ✅ |
| **CC026** | 避免在异步代码中使用 `SemaphoreSlim.Wait()`;请使用 `await WaitAsync()` | Warning | ✅ |
| **CC027** | 返回的 Task 使用了已释放的 `using` 资源 | Warning | ❌ |
| **CC028** | 避免在异步代码中调用阻塞式 `System.IO`(`File`、`StreamReader`、`StreamWriter`、`Stream`);请使用异步替代方法 | Warning | ✅ |
| **CC029** | 超时 `CancellationTokenSource` 应链接作用域内的 token(`CreateLinkedTokenSource` + `CancelAfter`) | Warning | ✅ |
| **CC030** | 避免在异步代码中调用阻塞式的 `Process.WaitForExit()`;请使用 `await WaitForExitAsync(token)` | Warning | ✅ |
| **CC031** | 避免在异步代码中使用阻塞式同步原语(`ManualResetEventSlim.Wait`、`WaitHandle.WaitOne`、`Monitor.Wait`、`Thread.Join`) | Warning | ❌ |
| **CC032** | 在非异步代码中丢弃了异步调用,而编译器的 CS4014 并未触发 | Warning | ❌ |
| **CC033** | 由类型创建但从未释放的 `CancellationTokenSource` 字段 | Warning | ❌ |
| **CC034** | 作用域内存在 token 时,创建的 `ParallelOptions` 却没有 `CancellationToken` | Warning | ✅ |
| **CC035** | 空的 `catch (OperationCanceledException)` 静默丢弃了 cancellation | Info | ❌ |
## 快速示例
### CC001: 缺失 CancellationToken 参数
```
// ❌ Warning CC001
public async Task ProcessDataAsync()
{
await Task.Delay(100);
}
// ✅ Fixed
public async Task ProcessDataAsync(CancellationToken cancellationToken = default)
{
await Task.Delay(100, cancellationToken);
}
```
### CC002: 未传递 Token
```
// ❌ 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);
}
```
### CC003: EF Core 缺失 Token
```
// ❌ Warning CC003
public async Task GetUserAsync(int id, CancellationToken cancellationToken)
{
return await _context.Users.FirstOrDefaultAsync(u => u.Id == id);
}
// ✅ Fixed
public async Task GetUserAsync(int id, CancellationToken cancellationToken)
{
return await _context.Users.FirstOrDefaultAsync(u => u.Id == id, cancellationToken);
}
```
### CC004: HttpClient 缺失 Token
```
// ❌ Warning CC004
public async Task FetchDataAsync(CancellationToken cancellationToken)
{
return await _httpClient.GetStringAsync("https://api.example.com");
}
// ✅ Fixed
public async Task FetchDataAsync(CancellationToken cancellationToken)
{
return await _httpClient.GetStringAsync("https://api.example.com", cancellationToken);
}
```
### CC005B: Controller 操作缺失 Token
```
// ❌ Warning CC005B
[HttpGet]
public async Task GetUsers()
{
var users = await _service.GetUsersAsync();
return Ok(users);
}
// ✅ Fixed - ASP.NET Core injects the token automatically
[HttpGet]
public async Task GetUsers(CancellationToken cancellationToken)
{
var users = await _service.GetUsersAsync(cancellationToken);
return Ok(users);
}
```
### CC005C: Minimal API 缺失 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 不是最后一个参数
```
// ℹ️ 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: 循环缺失 Cancellation 检查
```
// ❌ Warning CC009 - loop doesn't check for cancellation
public async Task ProcessItemsAsync(List- items, CancellationToken cancellationToken)
{
foreach (var item in items) // Could process 1M items without checking!
{
await ProcessAsync(item);
}
}
// ✅ Fixed
public async Task ProcessItemsAsync(List
- items, CancellationToken cancellationToken)
{
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessAsync(item);
}
}
```
### CC010: `await foreach` 缺失 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 迭代器 Token 缺失 `[EnumeratorCancellation]`
```
// ❌ Warning CC011 - WithCancellation can't deliver a token to this parameter
public async IAsyncEnumerable ReadAsync(CancellationToken token)
{
yield return await NextAsync(token);
}
// ✅ Fixed
public async IAsyncEnumerable ReadAsync([EnumeratorCancellation] CancellationToken token)
{
yield return await NextAsync(token);
}
```
### CC012: 作用域内存在 Token 时显式使用了 `CancellationToken.None`
```
// ℹ️ 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`
```
// ❌ 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: 未释放的 `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: 阻塞异步代码
```
// ❌ Warning CC015 - can deadlock and discards cancellation
public async Task RunAsync()
=> GetValueAsync().Result;
// ✅ Fixed
public async Task RunAsync()
=> await GetValueAsync();
```
### CC016: 未使用的 `CancellationToken` 参数
```
// ℹ️ 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` 忽略其停止 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 方法缺失 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: 宽泛的 `catch` 吞掉了 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 方法忽略 `ServerCallContext.CancellationToken`
```
// ❌ Warning CC020 - keeps running after the client cancels
public override async Task Handle(Request request, ServerCallContext context)
=> new Reply { Value = await _db.LoadAsync() };
// ✅ Fixed
public override async Task Handle(Request request, ServerCallContext context)
=> new Reply { Value = await _db.LoadAsync(context.CancellationToken) };
```
### CC21: 方法忽略 `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: 首选 `CancelAsync()` 而不是 `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: 转换为 `Action` 的 `async` Lambda
```
// ❌ 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: 针对 `IAsyncDisposable` 的 `await using`
```
// ℹ️ Info CC025 - Dispose() blocks on the async cleanup
using var resource = new AsyncResource();
// ✅ Fixed
await using var resource = new AsyncResource();
```
### CC026: 异步代码中的 `SemaphoreSlim.Wait()`
```
// ❌ 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: 返回的 Task 使用了已释放的 `using` 资源
```
// ❌ Warning CC027 - the stream is disposed before the returned task completes
public Task 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 ReadAsync(string path)
{
using var stream = File.OpenRead(path);
return await ReadAllBytesAsync(stream);
}
```
### CC028: 异步代码中的阻塞式 I/O
```
// ❌ Warning CC028 - blocks the thread for the whole disk read
public async Task 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 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);
}
```
### CC029: 超时 CTS 应链接作用域内的 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: 异步代码中阻塞式的 `Process.WaitForExit()`
```
// ❌ 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);
}
```
### CC031: 异步代码中阻塞式的同步原语
```
// ❌ 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);
}
```
### CC032: 非异步代码中未被 Await 的异步调用
```
// ❌ 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);
}
```
### CC033: 从未被释放的 `CancellationTokenSource` 字段
```
// ❌ 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();
}
```
### CC034: 缺失 `CancellationToken` 的 `ParallelOptions`
```
// ❌ 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,
};
```
### CC035: Cancellation 被空的 Catch 静默吞掉
```
// ❌ Info CC035 - the caller cannot tell the save did not happen
try
{
await SaveAsync(cancellationToken);
}
catch (OperationCanceledException)
{
}
```
## 配置
默认情况下所有规则均已启用。请在 `.editorconfig` 中配置严重程度:
```
[*.cs]
# 禁用规则
dotnet_diagnostic.CC001.severity = none
# 将规则设为 error(导致构建失败)
dotnet_diagnostic.CC002.severity = error
# 使 CC006 更加显眼
dotnet_diagnostic.CC006.severity = warning
```
## 兼容性和支持的框架
- 分析器程序集的目标框架是 **.NET Standard 2.0**,并基于 **Roslyn 4.8** 编译,兼容
Visual Studio 2022 17.8+ 和 .NET SDK 8+ 编译器宿主
- 使用者项目可以是兼容编译器宿主所支持的任何目标框架
- **ASP.NET Core**(Controllers、Minimal APIs、SignalR hubs、通过 `HttpContext.RequestAborted` 的 middleware)
- **托管服务**(`BackgroundService.ExecuteAsync`)
- **gRPC**(`ServerCallContext.CancellationToken`)
- **Entity Framework Core**(经过筛选的可取消查询和保存方法)
- **HttpClient**(经过筛选的可取消请求和内容方法)
- **MediatR**(IRequestHandler 实现)
- **异步流**(`IAsyncEnumerable`、`[EnumeratorCancellation]`)
- **ValueTask** 和 **ValueTask** 返回类型
## 项目质量
- **700+ 回归测试**,覆盖全面,外加一个跨分析器误报防护机制,该机制会
在每个符合惯用法的代码(核心、框架、嵌套作用域、特殊语法)上运行所有分析器,并断言
零诊断结果
- **测试驱动开发(TDD)** 方法
- 基于官方的 **Microsoft Roslyn API** 构建
- 遵循 **.NET Analyzer 最佳实践**(每条规则都有文档记录、进行发布跟踪,并由
`RuleCatalogTests` 漂移防护机制覆盖)
## 从源码构建
```
# 克隆仓库
git clone https://github.com/georgepwall1991/CancelCop.Analyzer.git
cd CancelCop.Analyzer
# 还原和构建
dotnet restore
dotnet build
# 运行测试
dotnet test
# 打包 NuGet package
dotnet pack src/CancelCop.Analyzer.Package/CancelCop.Analyzer.Package.csproj -c Release
```
## 项目结构
```
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
```
## 示例项目
`samples/CancelCop.Sample` 项目通过以下方式演示了分析器规则:
- 按诊断家族分组的针对性示例;
- 同时包含违规示例(触发警告)和正确的模式
- 解释每条规则重要性的详细注释
构建示例以查看分析器的实际运行情况:
```
dotnet build samples/CancelCop.Sample
```
## 贡献
欢迎贡献代码!请查看
[贡献指南](https://github.com/georgepwall1991/CancelCop.Analyzer/blob/main/CONTRIBUTING.md)。
关键要点:
- 遵循 TDD 方法(测试优先)
- 确保所有测试通过
- 为新功能更新文档
- 每个拉取请求(pull request)仅包含一个功能
## 路线图
CancelCop 现已发布 **29 条规则**,涵盖 token 存在性、传递、定位、循环检查、
异步流、阻塞性的 sync-over-async(包括阻塞式 File/StreamReader I/O)、资源
生命周期、async 规范以及框架 cancellation 源。最初在此处规划的功能已经发布(使用它们最终的 ID):
`CancellationToken.None` 误用 → **CC012**,未使用的 token 参数 → **CC016**,async void →
**CC023**。随着常见的 cancellation 陷阱浮现,会视情况 opportunistic 地添加新规则;错误修复
和误报强化工作将在每次发布中持续进行。
## 许可证
[MIT 许可证](https://github.com/georgepwall1991/CancelCop.Analyzer/blob/main/LICENSE)
## 作者
**George Wall** - [GitHub](https://github.com/georgepwall1991)
⭐ 如果 CancelCop 帮助你编写了更好的异步代码,请考虑给它点个 star!
标签:Roslyn分析器, 云安全监控, 代码规范, 多人体追踪, 安全专业人员, 异步编程, 静态分析