georgepwall1991/LinqContraband
GitHub: georgepwall1991/LinqContraband
一款基于 Roslyn 的 EF Core 静态分析器,在编译时检测数据访问层的性能反模式和安全隐患。
Stars: 9 | Forks: 0
# LinqContraband
` 传递给接受 `IEnumerable` 的方法的情况。这会阻止进一步的提供程序端组合并隐藏真实的查询成本。
**👶 像对 10 岁孩子一样解释:** 想象你有一张“自建汉堡”的优惠券。你把它给了厨师,但他不让你选择配料,而是立即递给你一个普通汉堡说“太晚了,我已经做好了。”
**❌ 罪行:**
```
public void ProcessUsers(IEnumerable users)
{
// Iterates and fetches ALL users from DB immediately.
foreach(var u in users) { ... }
}
// Passing IQueryable to IEnumerable parameter.
ProcessUsers(db.Users);
```
**✅ 修复:**
将参数更改为 `IQueryable` 以允许组合,或者如果您*打算*获取所有内容,请在调用站点显式调用 `.ToList()`。
```
public void ProcessUsers(IQueryable users)
{
// Now we can filter! SELECT ... WHERE Age > 10
foreach(var u in users.Where(x => x.Age > 10)) { ... }
}
```
### LC005: 多次 OrderBy 调用
这是一个逻辑错误,表现得像一个性能错误。第二个 OrderBy 完全忽略第一个。数据库为第一列创建排序计划,然后将其丢弃以按第二列排序。
**👶 像对 10 岁孩子一样解释:** 想象告诉某人按花色(红桃、黑桃……)对一副牌进行排序。他们刚一完成,你就说“实际上,按数字(2、3、4……)排序。”第一次排序的工作全都白费了,因为你改变了规则。
**❌ 罪行:**
```
// Sorts by Name, then immediately discards it to sort by Age.
var query = db.Users.OrderBy(u => u.Name).OrderBy(u => u.Age);
```
**✅ 修复:**
正确地链接它们。
```
var query = db.Users.OrderBy(u => u.Name).ThenBy(u => u.Age);
```
### LC006: 笛卡尔爆炸风险
如果 User 有 10 个 Orders,而 Order 有 10 个 Items,则获取所有数据会为每个 User 创建 100 行。如果有 1000 个 User,那就是传输了 100,000 行。`AsSplitQuery` 通过 3 个独立、干净的查询获取 Users、Orders 和 Items。
**👶 像对 10 岁孩子一样解释:** 想象老师问 30 个学生吃了什么。她不是得到 30 个答案,而是让每个学生单独列出他们吃的每一根薯条。你最终会得到数千个答案(“我吃了薯条 #1”,“我吃了薯条 #2”),而不是仅仅“我吃了薯条”。
**❌ 罪行:**
```
// Fetches Users * Orders * Roles rows.
var query = db.Users.Include(u => u.Orders).Include(u => u.Roles).ToList();
```
**✅ 修复:**
使用 `.AsSplitQuery()` 在单独的 SQL 查询中获取相关数据。
```
// Fetches Users, then Orders, then Roles (3 queries).
var query = db.Users.Include(u => u.Orders).AsSplitQuery().Include(u => u.Roles).ToList();
```
### LC007: N+1 循环者
数据库查询具有很高的固定开销(延迟、连接池)。执行 100 个查询比执行 1 个获取 100 个项目的查询慢约 100 倍。LC007 涵盖的范围不止 `Find(...)`:当显式加载、查询具体化、聚合和 EF 基于集合的执行器在可证明为 EF 支持的源上每次迭代运行一次时,它会捕获这些情况。
**👶 像对 10 岁孩子一样解释:** 想象你需要 10 个鸡蛋。你开车去商店,买*一个*鸡蛋,开车回家。再开车回去,买*一个*鸡蛋,开车回家。你这样做 10 次。你花了一整天开车,而不是一次性买一整盒。
**❌ 罪行:**
```
foreach (var id in ids)
{
// Executes 1 query per ID. Latency kills you here.
var user = db.Users.Find(id);
}
// Explicit loading is the same trap in a different disguise.
foreach (var user in db.Users.ToList())
{
db.Entry(user).Collection(u => u.Orders).Load();
}
```
**✅ 修复:**
在循环外批量获取数据。如果问题是显式加载,请使用预加载。
```
// Executes 1 query for all IDs.
var users = db.Users.Where(u => ids.Contains(u.Id)).ToList();
// LC007 can auto-fix this shape to eager loading.
var usersWithOrders = db.Users.Include(u => u.Orders).ToList();
```
### LC008: Sync-over-Async
在 Web 应用程序中,线程是有限的资源。阻塞线程等待 SQL (I/O) 意味着该线程无法为其他用户服务。在负载下,这会导致“线程饥饿”,即使 CPU 使用率很低,也会导致 503 错误。
**👶 像对 10 岁孩子一样解释:** 想象一个服务员接了你的单,然后走进厨房,盯着厨师 20 分钟直到食物准备好。没有其他人得到服务。这就是 Sync-over-Async。Async 意味着服务员接单后,在食物烹饪时去服务其他桌子。
**❌ 罪行:**
```
public async Task

### 阻止糟糕的查询被偷运到生产环境
[](https://www.nuget.org/packages/LinqContraband)
[](https://www.nuget.org/packages/LinqContraband)
[](LICENSE)
[](https://github.com/georgepwall1991/LinqContraband/actions/workflows/dotnet.yml)
[](https://github.com/georgepwall1991/LinqContraband/actions/workflows/dotnet.yml)
**LinqContraband** 是您 Entity Framework Core 查询的 TSA(安检局)。它会在您输入代码时进行扫描,没收那些性能杀手——例如客户端评估、N+1 风险和 Sync-over-Async——在它们进入生产环境之前。
### ⚡ 为什么使用 LinqContraband?
* **零运行时开销:** 它完全在编译时运行。对您的应用程序没有性能成本。
* **尽早发现 Bug:** 在 IDE 中修复 N+1 查询和笛卡尔爆炸,而不是在凌晨 3 点的停机期间。
* **强制执行最佳实践:** 充当你团队数据访问模式的自动化代码审查员。
* **通用支持:** 适用于 VS、Rider、VS Code 和 CI/CD 管道。兼容所有现代 EF Core 版本。
## 🚀 安装
通过 NuGet 安装。无需配置。
```
dotnet add package LinqContraband
```
分析器将立即开始扫描您的代码以查找违禁品。
## 👮♂️ 规则
### LC001: 本地方法走私者
当 EF Core 遇到无法翻译的方法时,它可能会切换到客户端评估(获取所有行)或抛出运行时异常。这会将快速的 SQL 查询变成巨大的内存泄漏。
**👶 像对 10 岁孩子一样解释:** 想象你雇了一位翻译把书翻译成西班牙语,但你使用了他不懂的俚语。他无法完成工作,所以他把整本字典递给你,说“你自己搞定。”你不得不阅读整本字典只是为了找一个词。
**❌ 罪行:**
```
// CalculateAge is a local C# method. EF Core doesn't know SQL for it.
var query = db.Users.Where(u => CalculateAge(u.Dob) > 18);
```
**✅ 修复:**
将逻辑提取到查询之外。
```
var minDob = DateTime.Now.AddYears(-18);
var query = db.Users.Where(u => u.Dob <= minDob);
```
明确映射用于翻译的方法是豁免的。LC001 不会报告标有 `[DbFunction]` 或 `EntityFrameworkCore.Projectables` 的 `[Projectable]` 属性的方法。
### LC002: 过早具体化
这是 EF Core 中的“Select *”。通过过早具体化,您通过网络传输整个表,在内存中丢弃其中的 99%,并让垃圾回收器忙碌不已。
**👶 像对 10 岁孩子一样解释:** 想象你想要一个意大利辣香肠披萨。你不是只点辣香肠,而是点了一个包含餐厅里所有配料的披萨。当它送到时,你必须花一个小时挑掉凤尾鱼、菠萝和蘑菇才能吃。这既浪费食物又浪费时间。
**❌ 罪行:**
```
// ToList() executes the query (SELECT * FROM Users).
// Where() then filters millions of rows in memory.
var query = db.Users.ToList().Where(u => u.Age > 18);
// Same crime with other materializers: AsEnumerable, ToDictionary, etc.
var query2 = db.Users.AsEnumerable().Where(u => u.Age > 18);
var query3 = db.Users.ToDictionary(u => u.Id).Where(kvp => kvp.Value.Age > 18);
// Redundant second materializer
var query4 = db.Users.ToArray().ToList();
```
**✅ 修复:**
保留在提供程序上执行的批准查询工作,然后一次性具体化。
```
// SELECT * FROM Users WHERE Age > 18
var query = db.Users.Where(u => u.Age > 18).ToList();
var query2 = db.Users.Where(u => u.Age > 18).ToDictionary(u => u.Id);
var query3 = db.Users.ToList();
```
**🛡️ 可靠性说明:**
- LC002 仅在 `IQueryable` 来源可证明时,遵循直接链、单赋值本地跳转和集合构造函数。
- 它对模糊的多重赋值、字段/属性来源以及没有明确 `IQueryable` 安全等效项的重载保持沉默。
- 修复器有意保持保守,并跳过有风险的情况,例如本地跳转重写或改变形状的具体化器,如 `ToDictionary(...).Where(...)`。
### LC003: 优先使用 Any() 而不是 Count() > 0
Count() > 0 强制数据库扫描所有匹配的行以返回总数(例如 5000)。Any() 生成 IF EXISTS (...),允许数据库在找到第一个匹配项后立即停止扫描。
**👶 像对 10 岁孩子一样解释:** 想象你想知道罐子里还有没有饼干。Count() > 0 就像把整个罐子倒在桌子上,一个个数了 500 块饼干才说“有”。Any() 就像打开盖子,看到一块饼干,然后立即说“有”。
**❌ 罪行:**
```
// Counts 1,000,000 rows just to see if one exists.
if (db.Users.Count() > 0) { ... }
```
**✅ 修复:**
```
// Checks IF EXISTS (SELECT 1 ...)
if (db.Users.Any()) { ... }
```
### LC004: 延迟执行泄漏
当 LinqContraband 能够证明被调用者实际上通过迭代、计数或具体化该参数来强制执行内存操作时,才会标记将 `IQueryable- > GetUsersAsync()
{
// Blocks the thread while waiting for DB.
return db.Users.ToList();
}
```
**✅ 修复:**
使用 Async 对应方法并 await 它。
```
public async Task
- > GetUsersAsync()
{
// Frees up the thread while waiting.
return await db.Users.ToListAsync();
}
```
### LC009: 跟踪税
EF Core 为它获取的每个实体拍摄一张“快照”以检测更改。对于只读仪表板,此快照过程会消耗 CPU 并使每行的内存使用量翻倍。
**👶 像对 10 岁孩子一样解释:** 想象你去博物馆。你承诺不触摸任何东西。但保安仍然跟着你,对你看到的每一幅画拍摄高分辨率照片,以防你决定在一幅画上画胡子。这浪费了他们的时间和内存。
**❌ 罪行:**
```
public List
- > GetUsers(CancellationToken ct)
{
// Violation: CancellationToken is ignored
return await db.Users.ToListAsync();
}
```
**✅ 修复:**
将 token 传递给异步方法。
```
public async Task
- > GetUsers(CancellationToken ct)
{
// Correct: Robot is listening!
return await db.Users.ToListAsync(ct);
}
```
### LC027: 缺少显式外键属性
没有相应 FK 属性的导航属性(例如 `public Customer Customer { get; set; }`)会导致 EF Core 在幕后创建一个“影子属性”。影子 FK 使得在不加载导航实体的情况下设置关系变得更加困难,产生效率较低的 API 序列化,并可能导致细微的性能问题。
**👶 像对 10 岁孩子一样解释:** 想象你把朋友的电话号码写在一张藏在你桌子底下的秘密便利贴上。当有人问“你朋友的号码是多少?”时,你必须爬到桌子底下才能找到它。如果你只是把号码写在你的地址簿里(显式 FK),任何人都可以立即查到。
**❌ 罪行:**
```
public class Order
{
public int Id { get; set; }
// No CustomerId — EF creates a shadow FK
public Customer Customer { get; set; }
}
```
**✅ 修复:**
添加显式 FK 属性。
```
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
}
```
### LC028: 深层 ThenInclude 链
深度超过 3 层的 `ThenInclude` 链会生成带有许多 LEFT JOIN 的复杂 SQL。这通常是过度获取的迹象——加载本应使用 `Select` 投影的深度嵌套数据。
**👶 像对 10 岁孩子一样解释:** 想象让你的朋友带上他们的妈妈,妈妈带上外婆,外婆带上曾外婆,曾外婆带上曾曾外婆。在某个时候,车满了,每个人都不舒服。最好只要求你真正需要的特定人员。
**❌ 罪行:**
```
// 4 levels deep — complex JOIN query
var orders = db.Orders
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.ThenInclude(a => a.Country)
.ThenInclude(c => c.Region)
.ThenInclude(r => r.Continent)
.ToList();
```
**✅ 修复:**
使用 `Select` 投影仅加载所需的嵌套数据。
```
var orders = db.Orders.Select(o => new {
o.Id,
CustomerName = o.Customer.Name,
Country = o.Customer.Address.Country.Name
}).ToList();
```
### LC029: 冗余标识 Select
`Select(x => x)` 告诉数据库“对于每一项,返回该项。”这是默认行为,所以把它写出来只会让你的代码更难阅读。
**👶 像对 10 岁孩子一样解释:** 想象你让朋友去商店买苹果。但随后你说,“对于你找到的每一个苹果,确保你带回来的苹果是一个苹果。”你的朋友会奇怪地看着你,因为他们本来就要那样做!
**❌ 罪行:**
```
// Violation: Does nothing
var users = db.Users.Select(u => u).ToList();
```
**✅ 修复:**
移除冗余的 Select。
```
// Correct
var users = db.Users.ToList();
```
### LC030: DbContext 生命周期审查
将 `DbContext` 作为字段或属性保存是一种生命周期异味。对于 scoped 类型可能没问题,但在长期存在的服务中是有风险的,因为 `DbContext` 不是线程安全的,且设计为短暂的。在保留它之前审查生命周期,对于长期存在的服务最好使用 `IDbContextFactory
标签:EF Core, N+1问题, NuGet包, ORM, Roslyn Analyzer, SOC Prime, SQL优化, 多人体追踪, 实体框架, 客户端评估, 开发工具, 性能优化, 最佳实践, 检测绕过, 编译器检查, 错误基检测, 静态代码分析