ai-developer-skill-os 8.1.7 → 8.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +3 -3
- package/.agents/skills/qk-code-review/SKILL.md +189 -0
- package/.agents/skills/qk-code-review/references/ai/ai-anti-patterns.md +28 -0
- package/.agents/skills/qk-code-review/references/ai/v8-schema-validation.md +65 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/architecture-review-guide.md +212 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/async-concurrency-patterns.md +515 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/code-quality-universal.md +358 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/code-review-best-practices.md +136 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/common-bugs-checklist.md +124 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/error-handling-principles.md +492 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/n-plus-one-queries.md +309 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/performance-review-guide.md +387 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/security-review-guide.md +318 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/sql-injection-prevention.md +308 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/xss-prevention.md +264 -0
- package/.agents/skills/qk-code-review/references/languages/angular.md +768 -0
- package/.agents/skills/qk-code-review/references/languages/c.md +890 -0
- package/.agents/skills/qk-code-review/references/languages/cpp.md +893 -0
- package/.agents/skills/qk-code-review/references/languages/csharp.md +519 -0
- package/.agents/skills/qk-code-review/references/languages/css-less-sass.md +661 -0
- package/.agents/skills/qk-code-review/references/languages/django.md +985 -0
- package/.agents/skills/qk-code-review/references/languages/fastapi.md +580 -0
- package/.agents/skills/qk-code-review/references/languages/go.md +993 -0
- package/.agents/skills/qk-code-review/references/languages/java.md +409 -0
- package/.agents/skills/qk-code-review/references/languages/java8.md +586 -0
- package/.agents/skills/qk-code-review/references/languages/kotlin.md +1018 -0
- package/.agents/skills/qk-code-review/references/languages/nestjs.md +593 -0
- package/.agents/skills/qk-code-review/references/languages/php.md +684 -0
- package/.agents/skills/qk-code-review/references/languages/python.md +1073 -0
- package/.agents/skills/qk-code-review/references/languages/qt.md +757 -0
- package/.agents/skills/qk-code-review/references/languages/react.md +871 -0
- package/.agents/skills/qk-code-review/references/languages/ruby.md +964 -0
- package/.agents/skills/qk-code-review/references/languages/rust.md +846 -0
- package/.agents/skills/qk-code-review/references/languages/svelte.md +1064 -0
- package/.agents/skills/qk-code-review/references/languages/swift.md +936 -0
- package/.agents/skills/qk-code-review/references/languages/typescript.md +1016 -0
- package/.agents/skills/qk-code-review/references/languages/vue.md +924 -0
- package/.agents/skills/qk-code-review/references/languages/zig.md +440 -0
- package/README.md +3 -3
- package/bin/install.js +5 -2
- package/package.json +1 -1
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# Hướng dẫn Review Code C# / .NET
|
|
2
|
+
|
|
3
|
+
> Hướng dẫn review code C# / .NET 8, bao quát các tính năng mới của C# 12, lập trình bất đồng bộ (Async), tối ưu hiệu năng EF Core, các best practice cho ASP.NET Core, Dependency Injection và LINQ.
|
|
4
|
+
|
|
5
|
+
## Mục lục
|
|
6
|
+
|
|
7
|
+
- [Tính năng mới của C# 12](#tính-năng-mới-của-c-12)
|
|
8
|
+
- [Lập trình Bất đồng bộ (Async)](#lập-trình-bất-đồng-bộ)
|
|
9
|
+
- [Hiệu năng EF Core](#hiệu-năng-ef-core)
|
|
10
|
+
- [Best Practice ASP.NET Core](#best-practice-aspnet-core)
|
|
11
|
+
- [Dependency Injection](#dependency-injection)
|
|
12
|
+
- [Best Practice LINQ](#best-practice-linq)
|
|
13
|
+
- [Review Checklist](#review-checklist)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Tính năng mới của C# 12
|
|
18
|
+
|
|
19
|
+
### Primary Constructors (Cho các class không phải record)
|
|
20
|
+
|
|
21
|
+
```csharp
|
|
22
|
+
// ❌ Constructor truyền thống với nhiều boilerplate code
|
|
23
|
+
public class ProductService
|
|
24
|
+
{
|
|
25
|
+
private readonly ProductDbContext _db;
|
|
26
|
+
private readonly ILogger<ProductService> _logger;
|
|
27
|
+
|
|
28
|
+
public ProductService(ProductDbContext db, ILogger<ProductService> logger)
|
|
29
|
+
{
|
|
30
|
+
_db = db;
|
|
31
|
+
_logger = logger;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ✅ Primary Constructor —— Dependency Injection cực kỳ ngắn gọn
|
|
36
|
+
public class ProductService(ProductDbContext db, ILogger<ProductService> logger)
|
|
37
|
+
{
|
|
38
|
+
public async Task<Product?> GetAsync(int id)
|
|
39
|
+
=> await db.Products.FindAsync(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ⚠️ Lưu ý: tham số của primary constructor KHÔNG PHẢI là property, không thể gán lại
|
|
43
|
+
// ⚠️ Nếu cần lưu trữ lâu dài, hãy khai báo field rõ ràng
|
|
44
|
+
public class OrderService(OrderDbContext db)
|
|
45
|
+
{
|
|
46
|
+
private readonly OrderDbContext _db = db; // Bắt tường minh
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Collection Expressions (Biểu thức tập hợp)
|
|
51
|
+
|
|
52
|
+
```csharp
|
|
53
|
+
// ❌ Khởi tạo tập hợp kiểu cũ
|
|
54
|
+
int[] nums = new int[] { 1, 2, 3 };
|
|
55
|
+
List<string> names = new List<string> { "alice", "bob" };
|
|
56
|
+
|
|
57
|
+
// ✅ Biểu thức tập hợp (Collection expressions)
|
|
58
|
+
int[] nums = [1, 2, 3];
|
|
59
|
+
List<string> names = ["alice", "bob"];
|
|
60
|
+
Span<char> span = ['a', 'b'];
|
|
61
|
+
|
|
62
|
+
// ✅ Spread operator (Toán tử trải)
|
|
63
|
+
int[] merged = [..nums, 4, 5];
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Default Lambda Parameters (Tham số mặc định cho Lambda)
|
|
67
|
+
|
|
68
|
+
```csharp
|
|
69
|
+
// ❌ Overload lambda
|
|
70
|
+
var add = (int a, int b) => a + b;
|
|
71
|
+
var addDefault = (int a) => a + 1;
|
|
72
|
+
|
|
73
|
+
// ✅ Sử dụng tham số mặc định
|
|
74
|
+
var add = (int a, int b = 1) => a + b;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Lập trình Bất đồng bộ (Async)
|
|
80
|
+
|
|
81
|
+
> 📖 Xem thêm các pattern đồng thời chung tại [Async & Concurrency Patterns](cross-cutting/async-concurrency-patterns.md)
|
|
82
|
+
|
|
83
|
+
### `Task.Wait()` / `.Result` / `async void` là các Anti-pattern cực kỳ nghiêm trọng
|
|
84
|
+
|
|
85
|
+
```csharp
|
|
86
|
+
// ❌ Task.Wait() —— Nguy cơ Deadlock (Block luồng đồng bộ bằng thao tác bất đồng bộ)
|
|
87
|
+
public ActionResult<Data> Get(int id)
|
|
88
|
+
{
|
|
89
|
+
var data = _service.GetDataAsync(id).Result; // Deadlock!
|
|
90
|
+
return Ok(data);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ❌ async void —— Không thể catch Exception, làm crash toàn bộ Process
|
|
94
|
+
public async void HandleEvent()
|
|
95
|
+
{
|
|
96
|
+
await _service.ProcessAsync(); // Nếu có lỗi sẽ crash thẳng
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ✅ async Task —— Bất đồng bộ toàn chuỗi (Async all the way)
|
|
100
|
+
public async Task<ActionResult<Data>> Get(int id)
|
|
101
|
+
{
|
|
102
|
+
var data = await _service.GetDataAsync(id);
|
|
103
|
+
return Ok(data);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `ConfigureAwait(false)` dành riêng cho code thư viện (Library code)
|
|
108
|
+
|
|
109
|
+
```csharp
|
|
110
|
+
// ❌ Code thư viện giữ lại SynchronizationContext không cần thiết
|
|
111
|
+
public class LibraryService
|
|
112
|
+
{
|
|
113
|
+
public async Task<string> GetDataAsync()
|
|
114
|
+
{
|
|
115
|
+
var response = await _httpClient.GetAsync("/api/data");
|
|
116
|
+
return await response.Content.ReadAsStringAsync();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ✅ Code thư viện dùng ConfigureAwait(false) để tránh deadlock
|
|
121
|
+
public class LibraryService
|
|
122
|
+
{
|
|
123
|
+
public async Task<string> GetDataAsync()
|
|
124
|
+
{
|
|
125
|
+
var response = await _httpClient.GetAsync("/api/data").ConfigureAwait(false);
|
|
126
|
+
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Truyền CancellationToken (CancellationToken Propagation)
|
|
132
|
+
|
|
133
|
+
```csharp
|
|
134
|
+
// ❌ Bỏ qua CancellationToken
|
|
135
|
+
public async Task<List<User>> SearchAsync(string query)
|
|
136
|
+
{
|
|
137
|
+
return await _db.Users.Where(u => u.Name.Contains(query)).ToListAsync();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ✅ Truyền CancellationToken xuyên suốt toàn chuỗi
|
|
141
|
+
public async Task<List<User>> SearchAsync(string query, CancellationToken ct = default)
|
|
142
|
+
{
|
|
143
|
+
return await _db.Users
|
|
144
|
+
.Where(u => u.Name.Contains(query))
|
|
145
|
+
.ToListAsync(ct);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Async Disposal (Giải phóng tài nguyên bất đồng bộ)
|
|
150
|
+
|
|
151
|
+
```csharp
|
|
152
|
+
// ❌ Giải phóng đồng bộ các tài nguyên bất đồng bộ
|
|
153
|
+
public class DataClient : IDisposable
|
|
154
|
+
{
|
|
155
|
+
public void Dispose()
|
|
156
|
+
{
|
|
157
|
+
_httpClient.Dispose(); // Có thể hủy luôn request đang chạy dang dở
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ✅ Dùng IAsyncDisposable
|
|
162
|
+
public class DataClient : IAsyncDisposable
|
|
163
|
+
{
|
|
164
|
+
public async ValueTask DisposeAsync()
|
|
165
|
+
{
|
|
166
|
+
await _stream.DisposeAsync();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ✅ Ở phía gọi (caller), dùng await using
|
|
171
|
+
await using var client = new DataClient();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Hiệu năng EF Core
|
|
177
|
+
|
|
178
|
+
### Vấn đề N+1 Queries
|
|
179
|
+
|
|
180
|
+
> 📖 Xem thêm lý thuyết chung tại [N+1 Queries Guide](cross-cutting/n-plus-one-queries.md)
|
|
181
|
+
|
|
182
|
+
```csharp
|
|
183
|
+
// ❌ Lỗi kinh điển N+1 —— Mỗi Blog query một lần để lấy Posts
|
|
184
|
+
foreach (var blog in await context.Blogs.ToListAsync())
|
|
185
|
+
{
|
|
186
|
+
foreach (var post in blog.Posts) // Chọc xuống DB mỗi vòng lặp!
|
|
187
|
+
{
|
|
188
|
+
Console.WriteLine(post.Title);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ✅ Eager Loading + Projection
|
|
193
|
+
await foreach (var blog in context.Blogs
|
|
194
|
+
.Select(b => new { b.Url, b.Posts })
|
|
195
|
+
.AsAsyncEnumerable())
|
|
196
|
+
{
|
|
197
|
+
foreach (var post in blog.Posts)
|
|
198
|
+
Console.WriteLine(post.Title);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Fetch thừa dữ liệu (Không dùng Projection)
|
|
203
|
+
|
|
204
|
+
```csharp
|
|
205
|
+
// ❌ Lấy toàn bộ các cột —— Chỉ cần dùng Url nhưng kéo cả table về
|
|
206
|
+
var urls = await context.Blogs.ToListAsync();
|
|
207
|
+
|
|
208
|
+
// ✅ Chỉ select (chiếu) đúng các cột cần thiết
|
|
209
|
+
var urls = await context.Blogs
|
|
210
|
+
.Select(b => b.Url)
|
|
211
|
+
.ToListAsync();
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Quên Phân trang (Missing Pagination)
|
|
215
|
+
|
|
216
|
+
```csharp
|
|
217
|
+
// ❌ Lấy nguyên mảng dữ liệu (Unbounded result set)
|
|
218
|
+
var posts = await context.Posts
|
|
219
|
+
.Where(p => p.Title.StartsWith("A"))
|
|
220
|
+
.ToListAsync(); // Có thể trả về hàng triệu dòng!
|
|
221
|
+
|
|
222
|
+
// ✅ Giới hạn số lượng kết quả
|
|
223
|
+
var posts = await context.Posts
|
|
224
|
+
.Where(p => p.Title.StartsWith("A"))
|
|
225
|
+
.OrderBy(p => p.Id)
|
|
226
|
+
.Skip((page - 1) * pageSize)
|
|
227
|
+
.Take(pageSize)
|
|
228
|
+
.ToListAsync();
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Cartesian Explosion (Bùng nổ dữ liệu do JOIN chéo)
|
|
232
|
+
|
|
233
|
+
```csharp
|
|
234
|
+
// ❌ Dùng nhiều Include tạo ra dữ liệu trùng lặp khổng lồ
|
|
235
|
+
var blogs = await context.Blogs
|
|
236
|
+
.Include(b => b.Posts)
|
|
237
|
+
.Include(b => b.Tags)
|
|
238
|
+
.ToListAsync(); // Dữ liệu Blog bị nhân bản ra mỗi dòng
|
|
239
|
+
|
|
240
|
+
// ✅ Sử dụng AsSplitQuery để tách query
|
|
241
|
+
var blogs = await context.Blogs
|
|
242
|
+
.Include(b => b.Posts)
|
|
243
|
+
.Include(b => b.Tags)
|
|
244
|
+
.AsSplitQuery()
|
|
245
|
+
.ToListAsync();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Quên dùng AsNoTracking cho câu truy vấn Read-only
|
|
249
|
+
|
|
250
|
+
```csharp
|
|
251
|
+
// ❌ Theo dõi mặc định —— Kể cả chỉ đọc cũng tốn chi phí Tracking
|
|
252
|
+
var products = await context.Products.ToListAsync();
|
|
253
|
+
|
|
254
|
+
// ✅ AsNoTracking —— Bỏ qua Change Tracking, chạy nhanh hơn và tốn ít RAM hơn
|
|
255
|
+
var products = await context.Products
|
|
256
|
+
.AsNoTracking()
|
|
257
|
+
.ToListAsync();
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Dùng hàm (Function) trên Cột làm mất tác dụng Index
|
|
261
|
+
|
|
262
|
+
```csharp
|
|
263
|
+
// ✅ Có thể xài index —— sargable
|
|
264
|
+
var posts1 = await context.Posts
|
|
265
|
+
.Where(p => p.Title.StartsWith("A"))
|
|
266
|
+
.ToListAsync();
|
|
267
|
+
|
|
268
|
+
// ❌ Không dùng được index —— Bắt buộc full table scan
|
|
269
|
+
var posts2 = await context.Posts
|
|
270
|
+
.Where(p => p.Title.EndsWith("A"))
|
|
271
|
+
.ToListAsync();
|
|
272
|
+
|
|
273
|
+
// ❌ Dùng hàm bao lấy cột —— Bắt buộc full table scan
|
|
274
|
+
var posts3 = await context.Posts
|
|
275
|
+
.Where(p => p.Title.ToLower() == "foo")
|
|
276
|
+
.ToListAsync();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Truy cập DB Đồng bộ vs Bất đồng bộ
|
|
280
|
+
|
|
281
|
+
```csharp
|
|
282
|
+
// ❌ Gọi DB đồng bộ —— Block luồng (Thread-blocking)
|
|
283
|
+
var products = context.Products.ToList();
|
|
284
|
+
context.SaveChanges();
|
|
285
|
+
|
|
286
|
+
// ✅ Gọi DB bất đồng bộ
|
|
287
|
+
var products = await context.Products.ToListAsync();
|
|
288
|
+
await context.SaveChangesAsync();
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Best Practice ASP.NET Core
|
|
294
|
+
|
|
295
|
+
### Sử dụng sai HttpClient
|
|
296
|
+
|
|
297
|
+
```csharp
|
|
298
|
+
// ❌ Tạo HttpClient mới cho mỗi Request —— Cạn kiệt Socket (Socket exhaustion)
|
|
299
|
+
using var client = new HttpClient();
|
|
300
|
+
var response = await client.GetAsync("https://api.example.com/data");
|
|
301
|
+
|
|
302
|
+
// ✅ Dùng IHttpClientFactory để tiêm (inject)
|
|
303
|
+
public class MyService
|
|
304
|
+
{
|
|
305
|
+
private readonly HttpClient _client;
|
|
306
|
+
public MyService(HttpClient client) => _client = client; // Lấy từ Factory
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Dùng HttpContext ở Background Thread
|
|
311
|
+
|
|
312
|
+
```csharp
|
|
313
|
+
// ❌ Bắt lấy Scoped service trong background task —— Lúc chạy thì request đã kết thúc và service bị dispose
|
|
314
|
+
_ = Task.Run(async () =>
|
|
315
|
+
{
|
|
316
|
+
await context.SaveChangesAsync(); // ObjectDisposedException!
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ✅ Tạo scope mới hoàn toàn
|
|
320
|
+
_ = Task.Run(async () =>
|
|
321
|
+
{
|
|
322
|
+
await using var scope = serviceScopeFactory.CreateAsyncScope();
|
|
323
|
+
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
324
|
+
await db.SaveChangesAsync();
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Đọc Request.Form đồng bộ
|
|
329
|
+
|
|
330
|
+
```csharp
|
|
331
|
+
// ❌ Đọc Form đồng bộ —— Lỗi sync-over-async
|
|
332
|
+
var form = HttpContext.Request.Form;
|
|
333
|
+
|
|
334
|
+
// ✅ Đọc Form bất đồng bộ
|
|
335
|
+
var form = await HttpContext.Request.ReadFormAsync();
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Dùng Exception để điều khiển luồng (Control flow)
|
|
339
|
+
|
|
340
|
+
```csharp
|
|
341
|
+
// ❌ Dùng exception để kiểm tra dữ liệu có tồn tại hay không —— Rất chậm
|
|
342
|
+
try
|
|
343
|
+
{
|
|
344
|
+
var user = await _db.Users.FirstAsync(u => u.Id == id);
|
|
345
|
+
}
|
|
346
|
+
catch (InvalidOperationException)
|
|
347
|
+
{
|
|
348
|
+
return NotFound();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ✅ Dùng câu lệnh kiểm tra (if/else) thay vì Exception
|
|
352
|
+
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
|
353
|
+
if (user is null) return NotFound();
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Cài Header sau khi Body đã được ghi
|
|
357
|
+
|
|
358
|
+
```csharp
|
|
359
|
+
// ❌ Body đã gửi nhưng lại đi set header —— Báo lỗi (Exception)
|
|
360
|
+
await next(context);
|
|
361
|
+
context.Response.Headers["X-Custom"] = "value"; // Sẽ văng lỗi!
|
|
362
|
+
|
|
363
|
+
// ✅ Dùng hàm callback OnStarting
|
|
364
|
+
context.Response.OnStarting(() =>
|
|
365
|
+
{
|
|
366
|
+
context.Response.Headers["X-Custom"] = "value";
|
|
367
|
+
return Task.CompletedTask;
|
|
368
|
+
});
|
|
369
|
+
await next(context);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Dependency Injection
|
|
375
|
+
|
|
376
|
+
### Inject Scoped Service vào Singleton
|
|
377
|
+
|
|
378
|
+
```csharp
|
|
379
|
+
// ❌ Scoped service tiêm vào Singleton —— Lệch vòng đời (Captive dependency)
|
|
380
|
+
services.AddSingleton<BackgroundWorker>();
|
|
381
|
+
services.AddScoped<IUserRepository, UserRepository>();
|
|
382
|
+
|
|
383
|
+
// BackgroundWorker là Singleton, UserRepository là Scoped
|
|
384
|
+
// → UserRepository sẽ bị dùng chung cho mọi request hoặc bị dispose sớm
|
|
385
|
+
|
|
386
|
+
// ✅ Tạo scope từ IServiceProvider ngay bên trong Singleton
|
|
387
|
+
public class BackgroundWorker : BackgroundService
|
|
388
|
+
{
|
|
389
|
+
private readonly IServiceScopeFactory _scopeFactory;
|
|
390
|
+
|
|
391
|
+
public BackgroundWorker(IServiceScopeFactory scopeFactory)
|
|
392
|
+
=> _scopeFactory = scopeFactory;
|
|
393
|
+
|
|
394
|
+
protected override async Task ExecuteAsync(CancellationToken ct)
|
|
395
|
+
{
|
|
396
|
+
await using var scope = _scopeFactory.CreateAsyncScope();
|
|
397
|
+
var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Best Practice LINQ
|
|
405
|
+
|
|
406
|
+
### Gọi LINQ sau khi đã ToList()
|
|
407
|
+
|
|
408
|
+
```csharp
|
|
409
|
+
// ❌ ToList rồi mới filter —— Kéo toàn bộ bảng lên RAM
|
|
410
|
+
var results = context.Posts
|
|
411
|
+
.Where(p => p.Title.StartsWith("A"))
|
|
412
|
+
.ToList()
|
|
413
|
+
.Where(p => SomeClientFilter(p)); // Filter phía Client (bộ nhớ)
|
|
414
|
+
|
|
415
|
+
// ✅ Để cho Database lo khâu filter nhiều nhất có thể
|
|
416
|
+
var results = await context.Posts
|
|
417
|
+
.Where(p => p.Title.StartsWith("A") && SomeDbFilter(p))
|
|
418
|
+
.AsAsyncEnumerable()
|
|
419
|
+
.Where(p => SomeClientFilter(p)) // Chỉ filter trên tập kết quả nhỏ
|
|
420
|
+
.ToListAsync();
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Count() vs Any()
|
|
424
|
+
|
|
425
|
+
```csharp
|
|
426
|
+
// ❌ Count() phải đếm toàn bộ dòng thỏa mãn
|
|
427
|
+
if (context.Users.Count() > 0) { /* ... */ }
|
|
428
|
+
|
|
429
|
+
// ✅ Any() hiệu quả hơn —— Ngừng tìm ngay khi thấy bản ghi đầu tiên
|
|
430
|
+
if (await context.Users.AnyAsync()) { /* ... */ }
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Duyệt (Enumerate) IEnumerable nhiều lần
|
|
434
|
+
|
|
435
|
+
```csharp
|
|
436
|
+
// ❌ IEnumerable bị duyệt 2 lần
|
|
437
|
+
public void Process(IEnumerable<int> numbers)
|
|
438
|
+
{
|
|
439
|
+
if (numbers.Any()) // Duyệt lần 1
|
|
440
|
+
{
|
|
441
|
+
foreach (var n in numbers) // Duyệt lần 2 (có thể gọi DB lại lần nữa)
|
|
442
|
+
{
|
|
443
|
+
Console.WriteLine(n);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ✅ Nếu cần dùng nhiều lần, hãy vật chất hóa (materialize) nó trước
|
|
449
|
+
public void Process(IEnumerable<int> numbers)
|
|
450
|
+
{
|
|
451
|
+
var list = numbers.ToList(); // Chỉ duyệt 1 lần duy nhất
|
|
452
|
+
if (list.Any())
|
|
453
|
+
{
|
|
454
|
+
foreach (var n in list)
|
|
455
|
+
{
|
|
456
|
+
Console.WriteLine(n);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Side-effects (tác dụng phụ) bên trong Select
|
|
463
|
+
|
|
464
|
+
```csharp
|
|
465
|
+
// ❌ Dùng Select để thực thi side-effect —— Không đoán trước được lúc nào nó chạy
|
|
466
|
+
var results = users.Select(u =>
|
|
467
|
+
{
|
|
468
|
+
_logger.LogInformation($"Processing {u.Name}"); // Side effect!
|
|
469
|
+
return u.Email;
|
|
470
|
+
}).ToList();
|
|
471
|
+
|
|
472
|
+
// ✅ Đưa Side-effect ra khỏi Select, bỏ vào vòng lặp foreach
|
|
473
|
+
foreach (var user in users)
|
|
474
|
+
{
|
|
475
|
+
_logger.LogInformation("Processing {Name}", user.Name);
|
|
476
|
+
}
|
|
477
|
+
var results = users.Select(u => u.Email).ToList();
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Review Checklist (Bảng kiểm nhanh)
|
|
483
|
+
|
|
484
|
+
### Tính năng mới C# 12
|
|
485
|
+
- [ ] Tham số của Primary constructor không bị gán lại lung tung.
|
|
486
|
+
- [ ] Cú pháp Collection expression (`[1, 2, 3]`) được dùng nhất quán.
|
|
487
|
+
|
|
488
|
+
### Lập trình Bất đồng bộ
|
|
489
|
+
- [ ] Tuyệt đối KHÔNG có `Task.Wait()`, `.Result`, hay `async void`.
|
|
490
|
+
- [ ] Code thư viện luôn dùng `ConfigureAwait(false)`.
|
|
491
|
+
- [ ] `CancellationToken` được truyền đi xuyên suốt.
|
|
492
|
+
- [ ] Tài nguyên bất đồng bộ được quản lý bởi `IAsyncDisposable` / `await using`.
|
|
493
|
+
- [ ] Không mix lẫn lộn truy cập dữ liệu đồng bộ và bất đồng bộ.
|
|
494
|
+
|
|
495
|
+
### EF Core
|
|
496
|
+
- [ ] Không có N+1 queries (Truy cập Navigation properties bên trong vòng lặp).
|
|
497
|
+
- [ ] Có dùng `Select()` (Projection) để tránh fetch thừa data.
|
|
498
|
+
- [ ] Lấy danh sách luôn có `Take()`/`Skip()` để Phân trang (Pagination).
|
|
499
|
+
- [ ] Sử dụng `AsSplitQuery()` nếu có quá nhiều `Include()`.
|
|
500
|
+
- [ ] Các query Read-only phải có `AsNoTracking()`.
|
|
501
|
+
- [ ] Cẩn thận với các Hàm thao tác trên Cột làm mất Index (Vd: `ToLower()`).
|
|
502
|
+
- [ ] Mọi cuộc gọi DB phải dùng hàm `Async`.
|
|
503
|
+
|
|
504
|
+
### ASP.NET Core
|
|
505
|
+
- [ ] Dùng `IHttpClientFactory` thay vì `new HttpClient()`.
|
|
506
|
+
- [ ] Task chạy nền (Background tasks) tự sinh ra Scope mới.
|
|
507
|
+
- [ ] Dùng `ReadFormAsync` thay thế cho `Request.Form`.
|
|
508
|
+
- [ ] Không xài Exception để check null hoặc điều khiển luồng.
|
|
509
|
+
- [ ] Header HTTP được xử lý qua `OnStarting`.
|
|
510
|
+
|
|
511
|
+
### Dependency Injection
|
|
512
|
+
- [ ] Không chích (Inject) đồ Scoped vào đồ Singleton.
|
|
513
|
+
- [ ] Background tasks luôn gọi CreateScope.
|
|
514
|
+
|
|
515
|
+
### LINQ
|
|
516
|
+
- [ ] Hạn chế gọi LINQ sau khi đã `ToList()` nếu query chưa tối ưu.
|
|
517
|
+
- [ ] Ưu tiên `Any()` hơn là `Count() > 0`.
|
|
518
|
+
- [ ] `IEnumerable` được lưu ra List nếu cần duyệt qua nhiều lần.
|
|
519
|
+
- [ ] Hàm `Select()` hoàn toàn pure, không có side-effects (như log, lưu DB).
|