01Phân biệt value type và reference type trong C#. struct khác class thế nào?Junior
Gợi ý trả lời
Value type (
int,bool,struct,enum) chứa trực tiếp dữ liệu, gán là sao chép giá trị. Reference type (class,string, mảng,delegate) chứa tham chiếu tới object trên heap, gán là sao chép tham chiếu.- Biến cục bộ value type thường nằm trên stack, nhưng nếu là field của class thì nằm trên heap cùng object đó.
structkhông kế thừa được, hợp với dữ liệu nhỏ, bất biến (toạ độ, tiền tệ). Nên khai báoreadonly struct.- Ép value type sang
objecthoặc interface gây boxing, tốn cấp phát.
var a = new Point(1, 2); // struct var b = a; b.X = 9; // a.X vẫn là 1Hay sai: nói "value type luôn nằm trên stack". Hãy nói về ngữ nghĩa sao chép thay vì vị trí bộ nhớ.
02async/await hoạt động thế nào? Vì sao không nên dùng .Result hay .Wait()?Junior
Gợi ý trả lời
async/awaitgiúp viết code bất đồng bộ như tuần tự: khi gặpawaitmột tác vụ chưa xong, method trả quyền điều khiển, thread được giải phóng, phần còn lại chạy tiếp khi tác vụ hoàn thành. Trình biên dịch biến method thành một state machine.- Async giúp không chặn thread khi chờ I/O (DB, HTTP), nhờ vậy server phục vụ được nhiều request hơn; nó không tự làm code chạy nhanh hơn.
- Việc nặng về CPU mới cần
Task.Run.
Gọi
.Resulthoặc.Wait()chặn thread đang chờ. Ở môi trường có SynchronizationContext (WinForms, WPF, ASP.NET cũ) dễ gây deadlock; ở ASP.NET Core gây cạn thread pool khi tải cao.Hay sai: dùng
async voidngoài event handler, exception sẽ không bắt được. Hãy trả vềTaskvà "async all the way".03Interface và abstract class khác nhau thế nào? Khi nào dùng cái nào?Junior
Gợi ý trả lời
Interface mô tả khả năng (hợp đồng) mà nhiều kiểu không liên quan có thể cùng thực hiện; abstract class là lớp cha dùng chung code và trạng thái cho một họ lớp gần nhau.
- Một class implement nhiều interface nhưng chỉ kế thừa một class.
- Abstract class có field, constructor, method có sẵn thân.
- Từ C# 8 interface có default implementation, nhưng vẫn không có field trạng thái.
Dùng interface cho các phụ thuộc cần thay thế hoặc mock trong test (
IEmailSender,IOrderRepository). Dùng abstract class khi các lớp con thật sự chia sẻ logic, vd.BaseHandlerxử lý log và retry chung.Người phỏng vấn muốn nghe: bạn ưu tiên composition và interface, chỉ dùng kế thừa khi có quan hệ "là một" rõ ràng.
04IEnumerable và IQueryable khác nhau thế nào khi dùng LINQ với Entity Framework?Junior
Gợi ý trả lời
IQueryablegiữ biểu thức truy vấn (expression tree) để provider như EF Core dịch thành SQL chạy trên DB.IEnumerablechạy LINQ trong bộ nhớ trên dữ liệu đã tải về.// Lọc trong SQL: chỉ tải đơn đã thanh toán var paid = db.Orders.Where(o => o.Status == Status.Paid).ToList(); // Tải cả bảng rồi mới lọc trong RAM IEnumerable<Order> all = db.Orders; var paid2 = all.Where(o => o.Status == Status.Paid).ToList();- Cả hai đều deferred execution: chỉ chạy khi duyệt hoặc gọi
ToList(),Count(),First()... - Duyệt cùng một
IEnumerablelazy nhiều lần có thể chạy truy vấn nhiều lần.
Hay sai: gọi
ToList()hoặcAsEnumerable()quá sớm rồi mới lọc, phân trang, khiến tải cả bảng về.- Cả hai đều deferred execution: chỉ chạy khi duyệt hoặc gọi
05Dependency Injection trong ASP.NET Core: phân biệt Transient, Scoped và Singleton.
Gợi ý trả lời
ASP.NET Core có DI container sẵn; lifetime quyết định khi nào tạo instance mới.
- Transient: mỗi lần resolve tạo mới. Hợp với service nhẹ, không trạng thái.
- Scoped: một instance cho mỗi request (mỗi scope).
DbContextmặc định là scoped. - Singleton: một instance cho cả vòng đời ứng dụng. Phải thread-safe.
builder.Services.AddScoped<IOrderService, OrderService>(); builder.Services.AddSingleton<IClock, SystemClock>();Hay sai: inject service scoped (như
DbContext) vào singleton, gọi là captive dependency: DbContext bị giữ mãi và dùng chung giữa nhiều thread. Môi trường Development sẽ báo lỗi khi validate scope. Nếu singleton cần dữ liệu scoped, hãy injectIServiceScopeFactoryvà tự tạo scope.06Middleware trong ASP.NET Core là gì? Thứ tự đăng ký có quan trọng không?
Gợi ý trả lời
Middleware là các bước nối thành pipeline xử lý request: mỗi bước có thể xử lý, gọi bước kế tiếp, rồi xử lý tiếp response trên đường về. Thứ tự rất quan trọng vì request đi qua đúng theo thứ tự đăng ký.
app.UseExceptionHandler("/error"); app.UseHttpsRedirection(); app.UseRouting(); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();- Exception handler phải đứng đầu để bắt lỗi của mọi bước sau.
UseAuthenticationphải trướcUseAuthorization, nếu không user luôn là ẩn danh.- Middleware có thể "short-circuit", vd. trả cache hoặc 401 mà không gọi
next.
Người phỏng vấn muốn nghe: bạn từng viết middleware riêng, vd. gắn correlation id hoặc log thời gian xử lý, và phân biệt được middleware với filter của MVC.
07Lỗi N+1 query trong Entity Framework Core là gì và xử lý thế nào?
Gợi ý trả lời
N+1 là khi lấy danh sách N bản ghi bằng 1 query, rồi mỗi bản ghi lại phát sinh thêm 1 query để lấy dữ liệu liên quan, thường do lazy loading hoặc truy vấn trong vòng lặp.
Cách xử lý:
- Eager loading:
.Include(o => o.Items). - Tốt hơn nữa là projection chỉ lấy cột cần:
.Select(o => new OrderDto(o.Id, o.Items.Count)). - Include nhiều collection gây "cartesian explosion" thì dùng
.AsSplitQuery(). - Màn hình chỉ đọc thì thêm
.AsNoTracking()để giảm chi phí change tracking.
Để phát hiện, bật log SQL của EF Core khi phát triển, hoặc dùng công cụ APM xem số query mỗi request.
Người phỏng vấn muốn nghe: bạn kiểm tra SQL thực sự được sinh ra thay vì tin LINQ trông gọn là chạy nhanh.
- Eager loading:
08Garbage Collector của .NET hoạt động thế nào? Khi nào cần IDisposable?
Gợi ý trả lời
GC quản lý heap theo thế hệ: object mới ở Gen 0, sống sót qua lần thu gom thì lên Gen 1, Gen 2. Phần lớn object chết trẻ nên thu gom Gen 0 rất rẻ. Object lớn (khoảng từ 85 KB) vào Large Object Heap và chỉ được thu cùng Gen 2.
GC chỉ thu hồi bộ nhớ managed. Tài nguyên khác như file, socket, kết nối DB cần giải phóng chủ động qua
IDisposable:await using var conn = new NpgsqlConnection(cs); using var stream = File.OpenRead(path);- Finalizer chỉ là lưới an toàn, chạy không xác định thời điểm và tốn chi phí.
Hay sai: tạo mới
HttpClientcho mỗi request rồi dispose, dễ cạn socket. Hãy dùngIHttpClientFactoryhoặc một instance dùng chung.09Ứng dụng ASP.NET Core bị memory leak hoặc CPU cao trên production. Bạn điều tra thế nào?Senior
Gợi ý trả lời
Thu thập số liệu trước, đoán sau: xem metrics, chụp dump hoặc trace, rồi phân tích.
- Theo dõi bằng
dotnet-counters: heap size, số lần GC từng gen, thread pool queue length, exception rate. - Leak bộ nhớ: chụp hai lần
dotnet-gcdumphoặcdotnet-dumpcách nhau một khoảng, so sánh loại object tăng và tìm ai đang giữ tham chiếu. - CPU cao hoặc treo:
dotnet-traceđể xem hot path; thread pool queue tăng thường do sync-over-async.
Nguyên nhân hay gặp: event handler không gỡ đăng ký, cache static không giới hạn kích thước, giữ
DbContextquá lâu, regex hoặc serialize lặp lại trong vòng lặp nóng.Người phỏng vấn muốn nghe: một ca thực tế bạn từng xử lý, công cụ đã dùng và kết quả trước/sau.
- Theo dõi bằng
10Xử lý concurrency khi nhiều request cùng cập nhật một bản ghi trong EF Core thế nào?Senior
Gợi ý trả lời
Hai hướng chính: optimistic concurrency (phát hiện xung đột khi lưu) và pessimistic (khoá trước khi sửa).
Optimistic với EF Core: đánh dấu một cột làm concurrency token, EF thêm điều kiện so sánh vào
UPDATEvà némDbUpdateConcurrencyExceptionnếu không có dòng nào bị cập nhật.public class Product { public int Stock { get; set; } [Timestamp] public byte[] RowVersion { get; set; } = []; }- Bắt exception, tải lại dữ liệu, rồi retry hoặc báo người dùng.
- PostgreSQL thường dùng cột hệ thống
xminlàm token. - Trường hợp đơn giản như trừ tồn kho có thể dùng một câu
ExecuteUpdateAsyncatomic kèm điều kiệnStock >= qty.
Người phỏng vấn muốn nghe: bạn cân nhắc mức độ tranh chấp: thấp thì optimistic, cao và nghiệp vụ quan trọng thì khoá hoặc đưa vào hàng đợi.
11Bạn tối ưu hiệu năng một Web API .NET chịu tải cao thế nào?Senior
Gợi ý trả lời
Bắt đầu bằng đo đạc (load test, APM, profiler), sau đó xử lý các điểm nghẽn phổ biến theo thứ tự tác động.
- I/O: async xuyên suốt, không sync-over-async; dùng connection pool đúng cách.
- Database: projection,
AsNoTracking, index đúng, tránh N+1; với bulk update dùngExecuteUpdateAsync. - Cache:
IMemoryCache, distributed cache (Redis),HybridCachetừ .NET 9, output caching cho response công khai. - Cấp phát bộ nhớ: giảm object tạm trong hot path, dùng
Span<T>,ArrayPool,System.Text.Jsonvới source generator. - Hạ tầng: response compression, rate limiting middleware, scale ngang khi stateless.
Luôn so sánh số liệu trước và sau (p95 latency, throughput, CPU, GC). Micro-optimize nên đo bằng BenchmarkDotNet.
Hay sai: tối ưu code C# trong khi 90% thời gian nằm ở query DB.
12Bạn tổ chức một dự án .NET lớn thế nào? Clean Architecture có luôn cần thiết?Senior
Gợi ý trả lời
Mục tiêu là tách nghiệp vụ khỏi chi tiết hạ tầng để dễ test và dễ thay đổi; mức độ tách nên tương xứng với độ phức tạp của dự án.
Clean Architecture điển hình có các project: Domain (entity, rule), Application (use case, interface), Infrastructure (EF Core, gửi mail, gọi API ngoài), Api (controller hoặc minimal API). Phụ thuộc chỉ hướng vào trong.
- Dịch vụ CRUD nhỏ thì cách tổ chức theo vertical slice (mỗi tính năng một thư mục) thường gọn hơn và ít boilerplate.
- Tránh bọc thêm repository chung chung lên EF Core nếu nó chỉ chuyển tiếp lời gọi.
- MediatR hay CQRS là công cụ, không phải yêu cầu bắt buộc.
Người phỏng vấn muốn nghe: bạn hiểu trade-off giữa số lớp trừu tượng và tốc độ phát triển, và nêu được lý do cho lựa chọn trong dự án gần nhất.
Gặp câu hỏi khác khi phỏng vấn? Kể lại ẩn danh để người sau ôn đúng trọng tâm, hoặc xem câu hỏi thật hay gặp ở nhiều công ty.