# C# / .NET Code Review Guide > C# / .NET 8 代码审查指南,覆盖 C# 12 新特性、异步编程、EF Core 性能、ASP.NET Core 最佳实践、依赖注入、LINQ 等核心主题。 ## 目录 - [C# 12 新特性](#c-12-新特性) - [异步编程](#异步编程) - [EF Core 性能](#ef-core-性能) - [ASP.NET Core 最佳实践](#aspnet-core-最佳实践) - [依赖注入](#依赖注入) - [LINQ 最佳实践](#linq-最佳实践) - [Review Checklist](#review-checklist) --- ## C# 12 新特性 ### Primary Constructors(非 record 类型) ```csharp // ❌ 样板代码过多的传统构造函数 public class ProductService { private readonly ProductDbContext _db; private readonly ILogger _logger; public ProductService(ProductDbContext db, ILogger logger) { _db = db; _logger = logger; } } // ✅ Primary Constructor——简洁的依赖注入 public class ProductService(ProductDbContext db, ILogger logger) { public async Task GetAsync(int id) => await db.Products.FindAsync(id); } // ⚠️ 注意:primary constructor 参数不是属性,不能被重新赋值 // ⚠️ 如果需要长期存储,显式声明字段 public class OrderService(OrderDbContext db) { private readonly OrderDbContext _db = db; // 显式捕获 } ``` ### Collection Expressions ```csharp // ❌ 传统集合初始化 int[] nums = new int[] { 1, 2, 3 }; List names = new List { "alice", "bob" }; // ✅ 集合表达式 int[] nums = [1, 2, 3]; List names = ["alice", "bob"]; Span span = ['a', 'b']; // ✅ 展开运算符 int[] merged = [..nums, 4, 5]; ``` ### Default Lambda Parameters ```csharp // ❌ 重载 lambda var add = (int a, int b) => a + b; var addDefault = (int a) => a + 1; // ✅ 默认参数 var add = (int a, int b = 1) => a + b; ``` --- ## 异步编程 ### Task.Wait() / .Result / async void 是严重反模式 ```csharp // ❌ Task.Wait() —— 死锁风险(同步阻塞异步操作) public ActionResult Get(int id) { var data = _service.GetDataAsync(id).Result; // 死锁! return Ok(data); } // ❌ async void —— 异常无法捕获,会崩溃进程 public async void HandleEvent() { await _service.ProcessAsync(); // 异常直接崩溃 } // ✅ async Task —— 全链路异步 public async Task> Get(int id) { var data = await _service.GetDataAsync(id); return Ok(data); } ``` ### ConfigureAwait(false) 用于库代码 ```csharp // ❌ 库代码不必要地捕获 SynchronizationContext public class LibraryService { public async Task GetDataAsync() { var response = await _httpClient.GetAsync("/api/data"); return await response.Content.ReadAsStringAsync(); } } // ✅ 库代码使用 ConfigureAwait(false) 避免死锁 public class LibraryService { public async Task GetDataAsync() { var response = await _httpClient.GetAsync("/api/data").ConfigureAwait(false); return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } } ``` ### CancellationToken 传播 ```csharp // ❌ 丢弃 CancellationToken public async Task> SearchAsync(string query) { return await _db.Users.Where(u => u.Name.Contains(query)).ToListAsync(); } // ✅ 全链路传递 CancellationToken public async Task> SearchAsync(string query, CancellationToken ct = default) { return await _db.Users .Where(u => u.Name.Contains(query)) .ToListAsync(ct); } ``` ### Async Disposal ```csharp // ❌ 同步 dispose 异步资源 public class DataClient : IDisposable { public void Dispose() { _httpClient.Dispose(); // 可能丢弃正在进行的请求 } } // ✅ IAsyncDisposable public class DataClient : IAsyncDisposable { public async ValueTask DisposeAsync() { await _stream.DisposeAsync(); } } // ✅ 调用方使用 await using await using var client = new DataClient(); ``` --- ## EF Core 性能 ### N+1 查询问题 ```csharp // ❌ 经典 N+1——每个 Blog 触发一次查询获取 Posts foreach (var blog in await context.Blogs.ToListAsync()) { foreach (var post in blog.Posts) // 每次循环都查询数据库! { Console.WriteLine(post.Title); } } // ✅ Eager Loading + 投影 await foreach (var blog in context.Blogs .Select(b => new { b.Url, b.Posts }) .AsAsyncEnumerable()) { foreach (var post in blog.Posts) Console.WriteLine(post.Title); } ``` ### 过度获取(不投影) ```csharp // ❌ 加载所有列——只需要 Url 时加载了全部字段 var urls = await context.Blogs.ToListAsync(); // ✅ 只投影需要的字段 var urls = await context.Blogs .Select(b => b.Url) .ToListAsync(); ``` ### 缺少分页 ```csharp // ❌ 无界结果集 var posts = await context.Posts .Where(p => p.Title.StartsWith("A")) .ToListAsync(); // 可能有百万条记录! // ✅ 限制结果数量 var posts = await context.Posts .Where(p => p.Title.StartsWith("A")) .OrderBy(p => p.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); ``` ### Cartesian Explosion(JOIN 笛卡尔爆炸) ```csharp // ❌ 多个 Include 创建大量重复数据 var blogs = await context.Blogs .Include(b => b.Posts) .Include(b => b.Tags) .ToListAsync(); // 每行重复 Blog 数据 // ✅ 使用 AsSplitQuery 拆分查询 var blogs = await context.Blogs .Include(b => b.Posts) .Include(b => b.Tags) .AsSplitQuery() .ToListAsync(); ``` ### 只读场景缺少 AsNoTracking ```csharp // ❌ 默认跟踪——只读查询也付出跟踪开销 var products = await context.Products.ToListAsync(); // ✅ AsNoTracking——跳过变更跟踪,更快且更省内存 var products = await context.Products .AsNoTracking() .ToListAsync(); ``` ### 列上函数阻止索引使用 ```csharp // ✅ 可以使用索引——sargable var posts1 = await context.Posts .Where(p => p.Title.StartsWith("A")) .ToListAsync(); // ❌ 无法使用索引——全表扫描 var posts2 = await context.Posts .Where(p => p.Title.EndsWith("A")) .ToListAsync(); // ❌ 列上套函数——全表扫描 var posts3 = await context.Posts .Where(p => p.Title.ToLower() == "foo") .ToListAsync(); ``` ### 同步 vs 异步数据库访问 ```csharp // ❌ 同步数据库调用——阻塞线程 var products = context.Products.ToList(); context.SaveChanges(); // ✅ 异步数据库调用 var products = await context.Products.ToListAsync(); await context.SaveChangesAsync(); ``` --- ## ASP.NET Core 最佳实践 ### HttpClient 误用 ```csharp // ❌ 每次请求创建新的 HttpClient——socket 耗尽 using var client = new HttpClient(); var response = await client.GetAsync("https://api.example.com/data"); // ✅ IHttpClientFactory 注入 public class MyService { private readonly HttpClient _client; public MyService(HttpClient client) => _client = client; // 从工厂注入 } ``` ### HttpContext 在后台线程中使用 ```csharp // ❌ 在后台任务中捕获 scoped 服务——请求结束后已释放 _ = Task.Run(async () => { await context.SaveChangesAsync(); // ObjectDisposedException! }); // ✅ 创建新的 scope _ = Task.Run(async () => { await using var scope = serviceScopeFactory.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); await db.SaveChangesAsync(); }); ``` ### Request.Form 同步访问 ```csharp // ❌ 同步读取 Form——sync over async var form = HttpContext.Request.Form; // ✅ 异步读取 var form = await HttpContext.Request.ReadFormAsync(); ``` ### 异常用于控制流 ```csharp // ❌ 用异常判断是否存在——异常开销大,比直接检查慢得多 try { var user = await _db.Users.FirstAsync(u => u.Id == id); } catch (InvalidOperationException) { return NotFound(); } // ✅ 使用检查而非异常 var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id); if (user is null) return NotFound(); ``` ### 响应头在 Body 之后设置 ```csharp // ❌ body 已发送后再设置 header——抛异常 await next(context); context.Response.Headers["X-Custom"] = "value"; // 可能抛异常! // ✅ 使用 OnStarting 回调 context.Response.OnStarting(() => { context.Response.Headers["X-Custom"] = "value"; return Task.CompletedTask; }); await next(context); ``` --- ## 依赖注入 ### Scoped 服务注入 Singleton ```csharp // ❌ Scoped 服务注入 Singleton——生命周期不匹配 services.AddSingleton(); services.AddScoped(); // BackgroundWorker 是 Singleton,UserRepository 是 Scoped // → UserRepository 在多个请求间共享或已释放 // ✅ 在 Singleton 中通过 IServiceProvider 创建 scope public class BackgroundWorker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; public BackgroundWorker(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory; protected override async Task ExecuteAsync(CancellationToken ct) { await using var scope = _scopeFactory.CreateAsyncScope(); var repo = scope.ServiceProvider.GetRequiredService(); } } ``` --- ## LINQ 最佳实践 ### ToList 之后再 LINQ ```csharp // ❌ 先 ToList 再过滤——全表加载到内存 var results = context.Posts .Where(p => p.Title.StartsWith("A")) .ToList() .Where(p => SomeClientFilter(p)); // 客户端过滤,已加载全部行 // ✅ 尽可能让数据库执行过滤 var results = await context.Posts .Where(p => p.Title.StartsWith("A") && SomeDbFilter(p)) .AsAsyncEnumerable() .Where(p => SomeClientFilter(p)) // 只过滤数据库返回的行 .ToListAsync(); ``` ### Count() vs Any() ```csharp // ❌ Count() 执行完整查询 if (context.Users.Count() > 0) { /* ... */ } // ✅ Any() 更高效——遇到第一条记录就返回 if (await context.Users.AnyAsync()) { /* ... */ } ``` ### 多次枚举 IEnumerable ```csharp // ❌ IEnumerable 被枚举两次 public void Process(IEnumerable numbers) { if (numbers.Any()) // 第一次枚举 { foreach (var n in numbers) // 第二次枚举(可能是重新查询) { Console.WriteLine(n); } } } // ✅ 如果需要多次使用,先物化 public void Process(IEnumerable numbers) { var list = numbers.ToList(); // 只枚举一次 if (list.Any()) { foreach (var n in list) { Console.WriteLine(n); } } } ``` ### Select 中的副作用 ```csharp // ❌ Select 中执行副作用——不可预测的执行时机 var results = users.Select(u => { _logger.LogInformation($"Processing {u.Name}"); // 副作用! return u.Email; }).ToList(); // ✅ 副作用放在 foreach 中 foreach (var user in users) { _logger.LogInformation("Processing {Name}", user.Name); } var results = users.Select(u => u.Email).ToList(); ``` --- ## Review Checklist ### C# 12 新特性 - [ ] Primary constructor 参数不被重新赋值 - [ ] 集合表达式语法一致(不混用新旧风格) ### 异步编程 - [ ] 无 `Task.Wait()`、`.Result`、`async void` - [ ] 库代码使用 `ConfigureAwait(false)` - [ ] `CancellationToken` 全链路传递 - [ ] 异步资源使用 `IAsyncDisposable` / `await using` - [ ] 不混用同步和异步数据访问 ### EF Core - [ ] 无 N+1 查询(导航属性在循环中访问) - [ ] 投影 `Select()` 避免过度获取 - [ ] 分页:`ToListAsync()` 前有 `Take()`/`Skip()` - [ ] 多个 `Include()` 使用 `AsSplitQuery()` - [ ] 只读查询使用 `AsNoTracking()` - [ ] 列上无函数调用阻止索引使用 - [ ] 数据库调用全部异步 ### ASP.NET Core - [ ] HttpClient 通过 `IHttpClientFactory` 获取 - [ ] 后台任务中不直接使用 scoped 服务 - [ ] 使用 `ReadFormAsync` 代替 `Request.Form` - [ ] 异常不用于控制流 - [ ] 响应头通过 `OnStarting` 设置 ### 依赖注入 - [ ] Scoped 服务不注入 Singleton - [ ] 后台任务创建新 scope ### LINQ - [ ] 无不必要的 `ToList()` 后再 LINQ - [ ] `Any()` 代替 `Count() > 0` - [ ] IEnumerable 不被多次枚举(或先物化) - [ ] Select 中无副作用