Ôn phỏng vấn C# & .NET

Ôn phỏng vấn

Câu hỏi phỏng vấn C# & .NET thường gặp (kèm gợi ý trả lời)

Câu hỏi phỏng vấn C# và .NET hay gặp: async/await, DI, EF Core, LINQ, garbage collector, middleware ASP.NET Core, kèm gợi ý trả lời. Gồm 12 câu do Drama Công Ty biên soạn: đọc ý chính rồi tự trả lời bằng ví dụ từ dự án của bạn.

  1. 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 đó.
    • struct khô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áo readonly struct.
    • Ép value type sang object hoặ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à 1
    

    Hay 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ớ.

  2. 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/await giúp viết code bất đồng bộ như tuần tự: khi gặp await mộ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 .Result hoặ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 void ngoài event handler, exception sẽ không bắt được. Hãy trả về Task và "async all the way".

  3. 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. BaseHandler xử 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.

  4. 04IEnumerable và IQueryable khác nhau thế nào khi dùng LINQ với Entity Framework?Junior

    Gợi ý trả lời

    IQueryable giữ biểu thức truy vấn (expression tree) để provider như EF Core dịch thành SQL chạy trên DB. IEnumerable chạ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 IEnumerable lazy nhiều lần có thể chạy truy vấn nhiều lần.

    Hay sai: gọi ToList() hoặc AsEnumerable() quá sớm rồi mới lọc, phân trang, khiến tải cả bảng về.

  5. 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). DbContext mặ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 inject IServiceScopeFactory và tự tạo scope.

  6. 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.
    • UseAuthentication phải trước UseAuthorization, 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.

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

  8. 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 HttpClient cho mỗi request rồi dispose, dễ cạn socket. Hãy dùng IHttpClientFactory hoặc một instance dùng chung.

  9. 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.

    1. Theo dõi bằng dotnet-counters: heap size, số lần GC từng gen, thread pool queue length, exception rate.
    2. Leak bộ nhớ: chụp hai lần dotnet-gcdump hoặc dotnet-dump cách nhau một khoảng, so sánh loại object tăng và tìm ai đang giữ tham chiếu.
    3. 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ữ DbContext quá 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.

  10. 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 UPDATE và ném DbUpdateConcurrencyException nế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 xmin làm token.
    • Trường hợp đơn giản như trừ tồn kho có thể dùng một câu ExecuteUpdateAsync atomic kèm điều kiện Stock >= 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.

  11. 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ùng ExecuteUpdateAsync.
    • Cache: IMemoryCache, distributed cache (Redis), HybridCache từ .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.Json vớ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.

  12. 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.