EngineeringSenior DeveloperInterview Sessions

Interview Answers - Phase 1: .NET Core & C# Mastery

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.Run trê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.CopyToAsync cấu hình mặc định hoặc load toàn bộ vào byte[] 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.
    1. Tắt Model Binding mặc định cho request đó để ASP.NET không tự buff data.
    2. Sử dụng MultipartReader đọc trực tiếp Request.Body rải rác từng chunk stream.
    3. 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: Task là reference type, mỗi lần return sẽ cấp phát (allocate) trên Heap. ValueTask là 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ùng ValueTask khô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.WhenAll trự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 IServiceScopeFactory vào Singleton, và mỗi khi cần thao tác thì using var scope = scopeFactory.CreateScope(), resolve DbContext bằng scope.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ùng ArrayPool<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.Json với luồng UTF8 cấp thấp (không convert sang CLR string).

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 Logger EventId.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.

Câu 9: Bulk CUD với EF Core 7/8+

  • Trả lời: Từ EF7, dùng ExecuteUpdateAsync và ExecuteDeleteAsync. Nó by-pass ChangeTracker sinh 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 đó.

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.

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 ConcurrentDictionary hay MemoryCache nộ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.RateLimiting hỗ trợ móc sẵn sang Redis provider mà không cần thiết kế bằng tay.