Chuyển tới nội dung chính

C# Best Practices

Định nghĩa (Definition)

Thực hành tốt nhất (Best Practices) C# là các hướng dẫn đã được chứng minh để viết mã dễ đọc, dễ bảo trì, hiệu suất cao và đáng tin cậy. Chúng bao gồm quy ước đặt tên, cấu trúc mã, xử lý ngoại lệ, thao tác chuỗi, tối ưu hiệu suất và các mẫu cụ thể cho ASP.NET Core.

Quy ước Đặt tên (Naming Conventions)

Đặt tên nhất quán làm cho mã tự tài liệu hóa và dễ điều hướng hơn cho các nhóm.

Thành phần (Element)Quy ước (Convention)Ví dụ
Lớp (Class), struct, recordPascalCaseOrderService, CustomerRecord
Giao diện (Interface)PascalCase với tiền tố IILogger, IRepository<T>
Phương thức (Method)PascalCaseCalculateTotal(), GetById()
Thuộc tính (Property)PascalCaseFirstName, OrderDate
Trường hằng số (Constant Field)PascalCaseMaxRetryCount, DefaultTimeout
Trường privatecamelCase với tiền tố __logger, _connectionString
Tham số phương thức (Method Parameter)camelCaseorderId, customerName
Biến cục bộ (Local Variable)camelCasetotalCount, isValid
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private const int MaxRetryCount = 3;

public OrderService(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}

public decimal CalculateTotal(Order order)
{
var subtotal = order.Items.Sum(i => i.Price * i.Quantity);
return subtotal;
}
}
Sử dụng tên có ý nghĩa

Một định danh được đặt tên tốt loại bỏ nhu cầu bình luận. Tránh các từ viết tắt khó hiểu — CalculateArea luôn tốt hơn Foo.

Cấu trúc Mã (Code Structure)

Khai báo Trường ở Trên cùng (Declare Fields at the Top)

Nhóm tất cả trường và hằng số ở đầu lớp. Điều này giúp trạng thái tổng thể của lớp hiển thị ngay lập tức:

public class Car
{
// Fields and constants at the top
private int _speed;
private readonly IEngine _engine;
private const int MaxSpeed = 250;

// Properties
public string Model { get; set; }

// Methods
public void Accelerate(int delta) { /* ... */ }
}

Trách nhiệm Đơn cho Phương thức (Single Responsibility for Methods)

Mỗi phương thức nên làm tốt một việc. Các phương thức lớn kết hợp nhiều trách nhiệm khó kiểm thử, gỡ lỗi và tái sử dụng:

// Bad — mixing concerns
public void ProcessOrder(Order order)
{
ValidateOrder(order);
CalculateTotal(order);
UpdateInventory(order);
SendConfirmation(order);
}

// Good — each method has one responsibility
public void ProcessOrder(Order order)
{
ValidateOrder(order);
var total = CalculateTotal(order);
UpdateInventory(order);
SendConfirmation(order);
}

Sử dụng Dấu ngoặc nhọn Nhất quán (Use Curly Braces Consistently)

Ngay cả cho câu lệnh if một dòng, luôn sử dụng dấu ngoặc nhọn. Nó ngăn lỗi khi thêm dòng sau này:

// Good
if (condition)
{
var userId = GetUserId();
}

// Risky — easy to break when adding more lines
if (condition)
var userId = GetUserId();

Sử dụng Bộ khởi tạo Đối tượng (Use Object Initializers)

Bộ khởi tạo đối tượng giảm lặp lại và cải thiện khả năng đọc:

// Verbose
var person = new Person();
person.FirstName = "John";
person.LastName = "Doe";
person.Age = 30;

// Concise
var person = new Person
{
FirstName = "John",
LastName = "Doe",
Age = 30
};

Xử lý Ngoại lệ (Exception Handling)

Bắt Ngoại lệ Cụ thể (Catch Specific Exceptions)

Tránh bắt kiểu Exception cơ sở. Chỉ bắt các ngoại lệ bạn có thể xử lý một cách có ý nghĩa:

// Bad — masks all errors
try
{
var result = 10 / divisor;
}
catch (Exception ex)
{
Log.Error($"Something went wrong: {ex.Message}");
}

// Good — targeted handling
try
{
var result = 10 / divisor;
}
catch (DivideByZeroException ex)
{
_logger.LogWarning(ex, "Attempted division by zero");
return 0;
}

Tránh Số và Chuỗi Ma thuật (Avoid Magic Numbers and Strings)

Thay thế giá trị được mã hóa cứng bằng hằng số có tên hoặc enum:

// Bad — what does "3" mean?
if (status == 3) { /* ... */ }

// Good — self-documenting
const int ActiveStatus = 3;
if (status == ActiveStatus) { /* ... */ }

// Better — use enums for discrete values
public enum OrderStatus
{
Pending = 0,
Processing = 1,
Shipped = 2,
Delivered = 3
}

if (order.Status == OrderStatus.Delivered) { /* ... */ }

Sử dụng Mệnh đề Bảo vệ (Use Guard Clauses)

Thất bại nhanh ở đầu phương thức để giữ cho logic chính sạch:

public void ProcessOrder(Order? order)
{
ArgumentNullException.ThrowIfNull(order);
ArgumentException.ThrowIfNullOrEmpty(order.Id);

if (order.Items.Count == 0)
throw new ArgumentException("Order must contain items.");

// Main logic starts here — all preconditions validated
FulfillOrder(order);
}

An toàn Null (Null Safety)

Luôn Thực hiện Kiểm tra Null (Always Perform Null Checks)

Sử dụng mẫu is not null hoặc toán tử có điều kiện null để ngăn NullReferenceException:

// Pattern matching check
List<string>? users = GetUsers();
if (users is not null)
{
foreach (var user in users)
{
Console.WriteLine(user);
}
}

// Null-conditional operator for property chains
var city = person?.Address?.City;

Sử dụng string.IsNullOrWhiteSpace cho Xác thực Đầu vào

if (string.IsNullOrWhiteSpace(userEmail))
{
Console.WriteLine("Email address is missing.");
}

Thực hành Tốt nhất với Chuỗi (String Best Practices)

Ưu tiên Nội suy Chuỗi (Prefer String Interpolation)

Nội suy chuỗi dễ đọc hơn nối chuỗi:

// Avoid
var message = "Hello, " + firstName + " " + lastName + "!";

// Prefer
var message = $"Hello, {firstName} {lastName}!";

// With formatting
var formattedPrice = $"Price: {price:C2}";

Sử dụng string.Empty Thay vì ""

Nó rõ ràng hơn và tránh sự mơ hồ:

// Avoid
if (name == "") { /* ... */ }

// Prefer
if (name == string.Empty) { /* ... */ }

So sánh Chuỗi Không phân biệt Chữ hoa/thường (Case-Insensitive String Comparison)

Luôn chuẩn hóa chữ hoa/thường trước khi so sánh với đầu vào người dùng:

if (string.Equals(input, "yes", StringComparison.OrdinalIgnoreCase))
{
// ...
}

Bộ sưu tập (Collections)

Sử dụng Any() Thay vì Count > 0

Any() ngắn mạch và tránh liệt kê toàn bộ bộ sưu tập:

// Bad — enumerates the entire collection
if (tasks.Count > 0) { /* ... */ }

// Good — stops at the first element
if (tasks.Any()) { /* ... */ }

// With predicate
if (tasks.Any(t => t.IsCompleted)) { /* ... */ }

Trả về Bộ sưu tập Lớn theo Trang (Return Large Collections Across Pages)

Không bao giờ tải toàn bộ tập dữ liệu lớn cùng lúc. Sử dụng phân trang để tránh OutOfMemoryException và thời gian phản hồi chậm:

[HttpGet]
public async Task<ActionResult<PagedResult<Order>>> GetOrders(
int page = 1, int pageSize = 20)
{
var orders = await _dbContext.Orders
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();

return Ok(new PagedResult<Order>(orders, totalCount));
}

Hiệu suất (Performance)

Sử dụng &&|| (Toán tử Ngắn mạch - Short-Circuit Operators)

Những toán tử này ngừng đánh giá ngay khi kết quả được xác định:

// Good — second condition is skipped if first is false
if (input.Length > 0 && input.StartsWith("prefix")) { /* ... */ }
if (role == "admin" || role == "supervisor") { /* ... */ }

Sử dụng var Có phân biệt (Use var Judiciously)

Sử dụng var khi kiểu rõ ràng từ vế phải của phép gán. Tránh khi kiểu không rõ ràng:

// Good — type is obvious
var name = "John Doe";
var orders = new List<Order>();

// Avoid — what does GetSomething return?
var x = GetSomething();

// Better — explicit type when unclear
Discount discount = CalculateDiscount(order);

Sử dụng using cho Tài nguyên Có thể Giải phóng (Use using for Disposable Resources)

Đảm bảo dọn dẹp đúng cách các tài nguyên như luồng tệp và kết nối cơ sở dữ liệu:

// Modern using declaration (C# 8+)
using var stream = new FileStream("data.txt", FileMode.Open);
// Automatically disposed when leaving scope

// Traditional using block
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();
// Automatically disposed when block exits
}

Chọn Kiểu Giá trị vs Kiểu Tham chiếu Có chủ đích (Choose Value Types vs Reference Types Intentionally)

  • Kiểu giá trị (Value Types) (struct, int, double) — lưu trữ trên ngăn xếp (Stack), hiệu quả cho dữ liệu nhỏ
  • Kiểu tham chiếu (Reference Types) (class, string) — lưu trữ trên heap, phù hợp cho cấu trúc dữ liệu phức tạp

Tiêm Phụ thuộc và Liên kết Lỏng (Dependency Injection and Loose Coupling)

Lập trình theo Giao diện (Program to Interfaces)

Phụ thuộc vào trừu tượng (Abstractions), không phải triển khai cụ thể, để cho phép khả năng kiểm thử và linh hoạt:

public interface ILogger
{
void Log(string message);
}

public class OrderService
{
private readonly IOrderRepository _repository;
private readonly ILogger _logger;

public OrderService(IOrderRepository repository, ILogger logger)
{
_repository = repository;
_logger = logger;
}
}

Không ghi nhận Dịch vụ Scoped trong Luồng Nền (Do Not Capture Scoped Services in Background Threads)

Dịch vụ scoped (như DbContext) được gắn với yêu cầu. Tạo scope mới trong công việc nền:

[HttpGet("/fire-and-forget")]
public IActionResult FireAndForget(
[FromServices] IServiceScopeFactory scopeFactory)
{
_ = Task.Run(async () =>
{
await using var scope = scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider
.GetRequiredService<AppDbContext>();

dbContext.Logs.Add(new LogEntry("Background task executed"));
await dbContext.SaveChangesAsync();
});

return Accepted();
}

Cụ thể ASP.NET Core (ASP.NET Core Specific)

Tránh Lệnh gọi Chặn (Avoid Blocking Calls)

Ứng dụng ASP.NET Core phải xử lý nhiều yêu cầu đồng thời. Sử dụng async/await xuyên suốt toàn bộ ngăn xếp lệnh gọi:

// Bad — blocks the thread
public ActionResult<Order> GetOrder(int id)
{
var order = _dbContext.Orders.Find(id); // synchronous
return order;
}

// Good — releases the thread while waiting
public async Task<ActionResult<Order>> GetOrder(int id)
{
var order = await _dbContext.Orders.FindAsync(id);
return order;
}
Không sử dụng Task.Run để biến API đồng bộ thành async

Task.Run lên lịch công việc trên thread pool nhưng không làm cho I/O đồng bộ trở thành async. Nó lãng phí một luồng thread pool. Thay vào đó, sử dụng API thực sự async.

Không Chặn trên Mã Async (Do Not Block on Async Code)

Không bao giờ gọi .Result hoặc .Wait() trên phương thức async — điều này gây cạn kiệt thread pool (Thread Pool Starvation):

// Bad — causes deadlock / thread pool starvation
var result = GetDataAsync().Result;
GetDataAsync().Wait();

// Good
var result = await GetDataAsync();

Gộp Kết nối HTTP với HttpClientFactory (Pool HTTP Connections with HttpClientFactory)

Không tạo và hủy các thực thể HttpClient trực tiếp — nó làm cạn kiệt socket:

// Bad — socket exhaustion
using var client = new HttpClient();

// Good — register in DI
builder.Services.AddHttpClient<IUserService, UserService>();

// Or use IHttpClientFactory directly
public class UserService
{
private readonly HttpClient _client;

public UserService(IHttpClientFactory factory)
{
_client = factory.CreateClient("UserApi");
}
}

Tránh I/O Đồng bộ trên Body Yêu cầu/Phản hồi (Avoid Synchronous I/O on Request/Response Body)

Kestrel không hỗ trợ đọc đồng bộ. Luôn sử dụng nạp chồng async:

// Bad — sync over async, blocks thread pool
var json = new StreamReader(Request.Body).ReadToEnd();

// Good — truly async
var json = await new StreamReader(Request.Body).ReadToEndAsync();

// Best — streaming deserialization
return await JsonSerializer.DeserializeAsync<Order>(Request.Body);

Sử dụng ReadFormAsync Thay vì Request.Form

// Bad — synchronous, can cause thread pool starvation
var form = HttpContext.Request.Form;

// Good — asynchronous
var form = await HttpContext.Request.ReadFormAsync();

Không Truy cập HttpContext từ Nhiều Luồng (Do Not Access HttpContext from Multiple Threads)

HttpContext không an toàn luồng (Not Thread-safe). Sao chép dữ liệu cần thiết trước khi thực thi song song:

// Bad — HttpContext accessed from multiple threads
private async Task<SearchResults> SearchAsync(string query)
{
_logger.LogInformation("Path: {Path}", HttpContext.Request.Path);
// ...
}

// Good — copy data before parallel work
var path = HttpContext.Request.Path;
var query1 = SearchAsync(SearchEngine.Google, query, path);
var query2 = SearchAsync(SearchEngine.Bing, query, path);
await Task.WhenAll(query1, query2);

Không Lưu trữ IHttpContextAccessor.HttpContext trong Trường (Do Not Store IHttpContextAccessor.HttpContext in a Field)

HttpContext chỉ hợp lệ trong yêu cầu đang hoạt động:

// Bad — captures null or stale context
public class MyService
{
private readonly HttpContext _context;
public MyService(IHttpContextAccessor accessor)
{
_context = accessor.HttpContext; // Often null in constructor
}
}

// Good — access HttpContext at the time of use
public class MyService
{
private readonly IHttpContextAccessor _accessor;
public MyService(IHttpContextAccessor accessor) => _accessor = accessor;

public void CheckAdmin()
{
var context = _accessor.HttpContext;
if (context is not null && !context.User.IsInRole("admin"))
throw new UnauthorizedAccessException();
}
}

Giảm thiểu Phân bổ Đối tượng Lớn (Minimize Large Object Allocations)

Đối tượng >= 85,000 byte nằm trên Heap Đối tượng Lớn (Large Object Heap - LOH) và yêu cầu GC Gen 2 đầy đủ để dọn dẹp:

  • Lưu vào bộ đệm các đối tượng lớn thường xuyên sử dụng
  • Sử dụng ArrayPool<T> cho bộ đệm lớn
  • Tránh phân bổ nhiều đối tượng lớn tồn tại ngắn trên các đường dẫn nóng (Hot Paths)

Giảm thiểu Ngoại lệ trong Đường dẫn Nóng (Minimize Exceptions in Hot Paths)

Ném và bắt ngoại lệ chậm. Không sử dụng ngoại lệ cho luồng điều khiển thông thường:

// Bad — exception for expected flow
try { return int.Parse(input); }
catch (FormatException) { return 0; }

// Good — no exception for expected cases
if (int.TryParse(input, out var result))
return result;
return 0;

Sử dụng Truy vấn No-Tracking cho Dữ liệu Chỉ đọc (Use No-Tracking Queries for Read-Only Data)

// When you don't need to update entities
var products = await _dbContext.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();

Không Sửa đổi Header Phản hồi Sau khi Body Đã Bắt đầu (Do Not Modify Response Headers After Body Has Started)

// Bad — may throw after response has started
app.Use(async (context, next) =>
{
await next();
context.Response.Headers["x-custom"] = "value"; // May fail
});

// Good — register callback before headers are flushed
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers["x-custom"] = "value";
return Task.CompletedTask;
});
await next();
});

Lỗi thường gặp (Common Pitfalls)

async void

Các phương thức async void không thể được await và ngoại lệ sẽ làm sập tiến trình. Luôn sử dụng async Task ngoại trừ cho trình xử lý sự kiện.

Sử dụng thuộc tính thay vì trường công khai

Trường công khai vi phạm tính đóng gói (Encapsulation). Sử dụng thuộc tính để kiểm soát truy cập và thêm xác thực:

// Bad
public class Student
{
public string Name;
}

// Good
public class Student
{
public string Name { get; set; }
}
Giả định HttpRequest.ContentLength không bao giờ null

ContentLengthnull khi không nhận được header Content-Length — nó không có nghĩa là zero. Kiểm tra như Request.ContentLength > 1024 trả về false ngay cả khi body vượt quá 1024 byte.

Xác thực tất cả đầu vào người dùng

Không bao giờ tin tưởng đầu vào người dùng. Luôn xác thực và làm sạch để ngăn ngừa SQL injection, XSS và các lỗ hổng khác.

Điểm chính (Key Takeaways)

  1. Tuân theo quy ước đặt tên nhất quán — PascalCase cho kiểu/thành phần, camelCase cho biến cục bộ/tham số, tiền tố _ cho trường private.
  2. Giữ phương thức nhỏ, tập trung vào một trách nhiệm duy nhất.
  3. Sử dụng async/await đầu đến cuối — không bao giờ chặn mã async với .Result hoặc .Wait().
  4. Bắt ngoại lệ cụ thể; tránh catch (Exception) ngoại trừ ở trình xử lý cấp cao nhất.
  5. Sử dụng HttpClientFactory thay vì tạo thực thể HttpClient trực tiếp.
  6. Giảm thiểu phân bổ đối tượng lớn và ngoại lệ trong các đường dẫn mã nóng.
  7. Lập trình theo giao diện và sử dụng tiêm phụ thuộc (Dependency Injection) cho liên kết lỏng.
  8. Luôn xác thực đầu vào người dùng và thực hiện kiểm tra null.
  9. Sử dụng Any() thay vì Count > 0 và nội suy chuỗi thay vì nối chuỗi.
  10. Không bao giờ truy cập HttpContext từ nhiều luồng hoặc lưu trữ nó trong trường.

Câu hỏi Phỏng vấn (Interview Questions)

Q: Tại sao nên tránh async void trong ASP.NET Core? Ngoại lệ ném trong phương thức async void không thể được bắt bởi người gọi và sẽ làm sập tiến trình thông qua ngữ cảnh đồng bộ hóa. Sử dụng async Task để framework có thể xử lý ngoại lệ đúng cách.

Q: Sự khác biệt giữa Task.Run và API thực sự async là gì? Task.Run lên lịch công việc đồng bộ lên thread pool, lãng phí một luồng. API thực sự async (ví dụ: ReadAsync) giải phóng hoàn toàn luồng trong khi chờ I/O hoàn thành, cho phép luồng phục vụ các yêu cầu khác.

Q: Tại sao nên sử dụng HttpClientFactory thay vì new HttpClient()? Mặc dù HttpClient triển khai IDisposable, việc hủy nó để lại socket ở trạng thái TIME_WAIT. Tạo và hủy nhiều thực thể làm cạn kiệt socket khả dụng. HttpClientFactory gộp và tái sử dụng các thực thể HttpClient.

Q: Heap Đối tượng Lớn (Large Object Heap) là gì và tại sao nó quan trọng? Đối tượng >= 85,000 byte được phân bổ trên LOH, yêu cầu thu gom rác (Garbage Collection) Gen 2 đầy đủ để dọn dẹp. Phân bổ LOH thường xuyên gây ra tạm dừng GC làm giảm hiệu suất trong ứng dụng web thông lượng cao.

Q: Tại sao sử dụng Any() thay vì Count > 0? Any() ngừng liệt kê tại phần tử đầu tiên, trong khi Count có thể liệt kê toàn bộ bộ sưu tập. Cho các nguồn IEnumerable<T> không phải ICollection<T>, sự khác biệt có thể đáng kể.

Q: Tại sao không nên ghi nhận dịch vụ scoped (như DbContext) trong luồng nền? Dịch vụ scoped được gắn với vòng đời yêu cầu HTTP. Sau khi yêu cầu kết thúc, scope bị hủy và dịch vụ đã ghi nhận trở nên không hợp lệ, gây ra ObjectDisposedException. Tạo scope mới với IServiceScopeFactory trong công việc nền.

Tài liệu tham khảo (References)