Best Practices
& Lưu ý
Do/Don't, pitfalls thường gặp, production checklist
Exception Handling
DO — Custom exception classes
throw new NotFoundException("Product", id);
Rõ ràng, pattern matching dễ dàng, map tự động sang status code.
DON'T — Generic exception
throw new Exception("Not found");
Không phân biệt được loại lỗi, luôn trả 500.
DO — Global ExceptionHandler
UseExceptionHandler() + IExceptionHandler
Một nơi duy nhất xử lý tất cả exceptions. DRY, nhất quán.
DON'T — Try-catch mỗi action
try { ... } catch { return 500; }
Copy-paste khắp nơi. Quên một chỗ = lộ stack trace.
DO — throw; (giữ stack trace)
catch (Exception ex) { Log(ex); throw; }
Stack trace nguyên vẹn, dễ debug đến dòng gây lỗi.
DON'T — throw ex; (mất stack trace)
catch (Exception ex) { Log(ex); throw ex; }
Stack trace bị reset, chỉ thấy dòng throw, mất nguồn gốc lỗi.
DO — ProblemDetails format
{ "type", "title", "status", "detail" }
Chuẩn RFC 7807. Client parse một lần, dùng mọi nơi.
DON'T — Ad-hoc format
{ "error": "...", "success": false }
Mỗi endpoint format khác nhau. Client phải xử lý nhiều dạng.
Logging
DO — Message template
"User {UserId} logged in", userId
Structured properties, queryable, performance tốt.
DON'T — String interpolation
$"User {userId} logged in"
Mất structured data, không query được, allocate string.
DO — Đúng log level
LogWarning("Product {Id} not found")
Warning cho "expected failure", Error cho "unexpected failure".
DON'T — Mọi thứ là LogInformation
LogInformation("ERROR: Product not found")
Không filter được, mất ý nghĩa của log levels.
DO — Log exception object
LogError(ex, "Failed to process {OrderId}", id)
Exception object được log riêng với stack trace đầy đủ.
DON'T — Chỉ log message
LogError("Error: " + ex.Message)
Mất stack trace, mất inner exception, mất exception type.
Pitfalls thường gặp
Empty catch block che giấu bug. App có vẻ chạy tốt nhưng thực ra đang fail silently. Dữ liệu bị mất, logic bị skip mà không ai biết.
// NGUY HIỂM: nuốt exception try { await ProcessOrder(); } catch { } // Bug ẩn nấp ở đây! // ĐÚNG: log rồi xử lý try { await ProcessOrder(); } catch (Exception ex) { _logger.LogError(ex, "Order processing failed"); throw; // Hoặc xử lý phù hợp }
Log chứa password, token, PII = vi phạm bảo mật + GDPR. Log files thường ít được bảo vệ hơn database. Attacker đọc log = có credentials.
// NGUY HIỂM: lộ password trong log _logger.LogInformation( "Login attempt: {Email} / {Password}", email, password); // KHÔNG BAO GIỜ! // ĐÚNG: chỉ log thông tin an toàn _logger.LogInformation( "Login attempt for {Email}", email);
throw ex; reset stack trace về dòng hiện tại. Bạn mất thông tin exception
ban đầu throw từ đâu — debug cực khó.
// SAI: throw ex; mất nguồn gốc catch (Exception ex) { _logger.LogError(ex, "Error"); throw ex; // Stack trace chỉ thấy dòng này! } // ĐÚNG: throw; giữ nguyên stack trace catch (Exception ex) { _logger.LogError(ex, "Error"); throw; // Stack trace nguyên vẹn từ nguồn }
Log mọi thứ = log không gì cả. Quá nhiều log khiến: disk đầy nhanh, performance giảm, tìm thông tin quan trọng như mò kim đáy bể.
- Đừng log mỗi dòng code — chỉ log tại ranh giới (entry/exit point, error, quyết định quan trọng)
- Đừng log toàn bộ request/response body — chỉ log ID và metadata
- Đừng log trong vòng lặp — log trước/sau vòng lặp với count
Không có Correlation ID = không thể trace request. Khi production có 1000 requests/giây, làm sao bạn tìm tất cả log liên quan đến một request cụ thể?
Giải pháp: thêm CorrelationId middleware (xem Kết hợp trong Thực tế). Mỗi request có một ID duy nhất xuyên suốt mọi log entry.
Debug logs trong production = thảm họa. EF Core log mọi SQL query ở Debug level. Nếu
production set Default: Debug, mỗi request tạo hàng chục dòng log SQL.
// appsettings.Production.json — PHẢI configure riêng { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Microsoft.EntityFrameworkCore": "Warning" } } }
Production Checklist
UseExceptionHandler())AddProblemDetails())appsettings.Production.json)/health)