Các câu trả lời dưới đây mang tính chất minh họa mindset của một Master/Architect: không chỉ đọc thuộc tài liệu mà còn hiểu trade-off và ứng dụng thực tiễn.
1. Async/Await & Concurrency
Câu 1: Tại sao gọi Task.Run trong ASP.NET là bad practice?
- Trả lời: ASP.NET Core không có khái niệm "request thread" chuyên biệt. Mọi request đều được map vào một thread thuộc Thread Pool ngay từ đầu. Gọi
Task.Run()trong controller chỉ để offload việc nặng sang một thread khác cũng lấy từ cùng Thread Pool đó. - Hậu quả: 1 request thay vì dùng 1 thread lại cấp thêm thẻ lấy 1 thread nữa (tổng cộng chiếm dụng context switch 2 thread vô ích). Nếu Traffic nhảy vọt, Thread Pool sẽ cạn kiệt (Starvation).
- Trường hợp dùng: Chỉ dùng
Task.Runtrên Desktop/Mobile app để offload tính toán nặng khỏi UI Thread (vì UI Thread là duy nhất và bị freeze). Trong HTTP request, nếu việc cực nặng, hãy nhét nó vào Background Service (Hangfire/RabbitMQ).
Câu 2: API upload file 500MB tối ưu Memory
- Trả lời (Memory Spike): Dùng
IFormFile.CopyToAsynccấu hình mặc định hoặc load toàn bộ vàobyte[]sẽ đẩy object lên LOH (Large Object Heap), gây GC collection Gen2 đắt đỏ làm chậm server. - Giải pháp Architect: Truyền stream thay vì buffer ngầm.
- Tắt Model Binding mặc định cho request đó để ASP.NET không tự buff data.
- Sử dụng
MultipartReaderđọc trực tiếpRequest.Bodyrải rác từng chunk stream. - Chunk đọc được GHI trực tiếp xuống Network Stream (Azure Blob) hoặc FileStream (
FileOptions.Asynchronous). Tức là File 500MB sẽ đi qua App của ta theo từng tảng 4KB (với ArrayPool), ko bao giờ tạo mảng 500MB tren RAM.
Câu 3: Task vs ValueTask
- Trả lời:
Tasklà reference type, mỗi lần return sẽ cấp phát (allocate) trên Heap.ValueTasklà kiểu struct, cấp phát trên Stack (không tốn phí alloc Heap). - Use case: Nếu method truy xuất Cache (Ví dụ
GetUserAsync()), có tới 90% khả năng dữ liệu đã có trong Redis/Memory (hoàn thành đồng bộ - synchronously) $\implies$ DùngValueTaskkhông phải alloc Task vô tội vạ. Chỉ khi cache miss, nó mới alloc Task ngầm bọc trong ValueTask. - Master note: ValueTask không được await 2 lần, không được
Task.WhenAlltrực tiếp.
Câu 4: Captive Dependency trong DI
- Trả lời: Xảy ra khi một service vòng đời dài (Singleton) inject vào một service vòng đời ngắn (Scoped/Transient).
- Hậu quả: Singleton cache DBContext (Scoped) trong constructor của nó. Bất kỳ request nào sau này gọi Singleton đều dùng lại một instance DbContext đầu tiên đó. 1 DbContext chia sẻ cho n threads đồng thời sẽ sinh crash
InvalidOperationException(vì DbContext ko thread-safe), hoặc connection leak, transaction bẩn lẫn lộn giữa các Request. - Cách fix: Inject
IServiceScopeFactoryvào Singleton, và mỗi khi cần thao tác thìusing var scope = scopeFactory.CreateScope(), resolve DbContext bằngscope.ServiceProvider.
2. Memory Management & Performance
Câu 5: Tại sao gọi GC.Collect() thủ công thường có hại?
- Trả lời: GC.Collect() force engine quét toàn bộ vòng đời (đặc biệt là Gen 2/LOH). Nó stop-the-world cực đoan. .NET tự điều chỉnh lịch trình GC dựa theo phân bổ Heuristics để đạt tỷ lệ tối ưu nhất giữa "Pause time" và "Memory clean". Can thiệp thủ công sẽ phá vỡ thuật toán self-tuning này, đẩy các objects LẼ RA chết dọn thành Gen 1/Gen 2, tạo gánh nặng vĩnh viễn (Mid-Life crisis allocation).
Câu 6: Tối ưu Background Service 10,000 MQTT msgs/s
- Giải pháp Architect:
- Tránh cấp phát
byte[]cho từng message: DùngArrayPool<byte>.Shared.Rent()để mượn array từ pool thay vìnew byte[], dùng xong.Return(). - Tránh chuỗi Immutable alloc (dấu hiệu: parse JSON string): Làm việc trực tiếp trên Memory/Stream. Chuyển đổi Parser dùng
ReadOnlySpan<byte>thay cho string manipulation. - Sử dụng
System.Text.Jsonvới luồng UTF8 cấp thấp (không convert sang CLR string).
- Tránh cấp phát
Câu 7: string.Intern() gây memory leak?
- Trả lời: Intern table duy trì một pool các reference String global nhằm tiết kiệm RAM cho các string lặp liên tục. Tuy nhiên, bất kỳ string nào add vào Intern Pool bằng
string.Intern()sẽ KHÔNG BAO GIỜ BỊ GC THU HỒI cho đến khi Process/AppDomain sập. Nếu nội dung string đó user gen ngẫu nhiên, ta sẽ đối mặt với memory leak vĩnh viễn.
3. Entity Framework Core Khắc Nghiệt
Câu 8: N+1 & Query Strategies
- Trả lời:
- N+1: 1 query load Order, N query load Carts của Order khi lặp
foreach. Detect: SQL Server Profiler / EF Core LoggerEventId.QueryExecuted. Include: Tốt, ra lệnh LEFT JOIN tự động lấy tất cả trong 1 query bự. Nhược điểm: Bị nổ Cartesian Explosion nếu Select Order + Items + Tags (nhân n x m rows dữ liệu dư thừa trên đường truyền).AsSplitQuery(): EFCore sẽ tách thành 3 query SQL riêng (Select Order,Select Items,Select Tags) giảm nghẽn Mạng vì ko join, EF tự rắp nối object in-memory.Select()(Projection): Best practice. Map đúng các properties thành DTO (ko lấy full table). N+1 bay màu tự nhiên.
- N+1: 1 query load Order, N query load Carts của Order khi lặp
Câu 9: Bulk CUD với EF Core 7/8+
- Trả lời: Từ EF7, dùng
ExecuteUpdateAsyncvàExecuteDeleteAsync. Nó by-passChangeTrackersinh luôn 1 câu UPDATE/DELETE trên DB mà KHÔNG load Entities lên RAM.- Vd:
await context.Users.Where(u => !u.IsActive).ExecuteUpdateAsync(s => s.SetProperty(u => u.Role, "Banned"));. - Nếu Insert Bulk khổng lồ? Cân nhắc dùng EFCore.BulkExtensions hoặc switch sang
SqlBulkCopy/ Dapper cho đoạn code pure đó.
- Vd:
Câu 10: ChangeTracker và AsNoTracking
- Trả lời: Mặc định EF lưu 1 Snapshot ẩn (shadow copy của value cũ) tại thời điểm Select lên, ghim trong RAM. Khi gọi DbContext.SaveChanges(), EF đối chiếu Entity hiện tại với Snapshot $\implies$ nếu khác, sinh câu SQL.
- Vì vậy Tracking x2 RAM cho mỗi Entity, x1 CPU do hàm đối chiếu chạy.
AsNoTracking()bypass 2 tác vụ đó, tiết kiệm 50% Memory/Time overhead.
- Vì vậy Tracking x2 RAM cho mỗi Entity, x1 CPU do hàm đối chiếu chạy.
4. Middleware & Khởi Chạy
Câu 11: UseAuthorization() đặt trước UseAuthentication()
- Trả lời: Pipeline chạy tuân tự (Russian Doll model).
UseAuthentication()là bước Giải Mã Token (JwtBearer) ra thành Context Object (User.Identity.IsAuthenticated).UseAuthorization()là bước Chặn Cửa nếu role báo false. Đặt AuthZ trước thì lúc chạy qua nó, Context.User hoàn toàn chưa được thiết lập (hoặc Un-authenticated), mọi Request sẽ vĩnh viễn ăn 401/403 (Unauthorized/Forbidden).
Câu 12: Rate limit custom trong mội trường Distributed
- Trả lời: Không dùng
ConcurrentDictionaryhayMemoryCachenội bộ API vì các Pod độc lập nhau sẽ chia sẻ sai (Limit được 10Req/s, Client hit qua LoadBalancer sẽ dính 10 x số Pods). - Giải pháp Architect: Sử dụng In-Memory Cache ngoài vi (Redis).
- Kết hợp Lua Script trên Redis cập nhật các thuật toán RateLimit kinh điển như Token Bucket hoặc Fixed Window để tránh round-trip chậm trễ (tăng số biến đếm và TTL expire cùng lúc 1 atomic transaction trên Redis).
- Hiện tại .NET 7/8 đã build-in
Microsoft.AspNetCore.RateLimitinghỗ trợ móc sẵn sang Redis provider mà không cần thiết kế bằng tay.