Lộ trình 7 ngày
Ngày 5 — ASP.NET Core và EF Core
Middleware pipeline trace, DI lifetimes + captive dependency, EF Core change tracking internals, loading strategies với N+1 demo, migrations.
Mục tiêu ngày 5
Sau ngày 5 bạn phải:
- Trace được HTTP request đi qua middleware pipeline ra sao.
- Phân biệt 3 DI lifetimes + hiểu captive dependency.
- Thiết kế REST API đúng convention + JWT auth.
- Hiểu EF Core change tracking ở mức cơ chế.
- Demo được N+1 problem và 4 cách fix.
- Biết khi nào dùng Migration vs Database-first.
🟢 Intern | 🔵 Junior | 🟡 Mid | 🔴 Senior
1. Kiến trúc ASP.NET Core
1.1. Hành trình của 1 HTTP request
Client (Browser/Postman)
│
▼ HTTP Request
┌────────────────────────────┐
│ Kestrel (web server) │ ← lắng nghe port 5000/5001
└────────────────────────────┘
│
▼
┌────────────────────────────┐
│ Middleware Pipeline │ ← thứ tự QUAN TRỌNG
│ ┌──────────────────────┐ │
│ │ Exception handler │ │ ← bao quanh toàn pipeline
│ │ ┌──────────────────┐ │ │
│ │ │ HTTPS redirect │ │ │
│ │ │ ┌──────────────┐ │ │ │
│ │ │ │ Routing │ │ │ │
│ │ │ │ ┌──────────┐ │ │ │ │
│ │ │ │ │ Auth │ │ │ │ │
│ │ │ │ │ ┌──────┐ │ │ │ │ │
│ │ │ │ │ │Authz │ │ │ │ │ │
│ │ │ │ │ │ ┌──┐ │ │ │ │ │ │
│ │ │ │ │ │ │EP│ │ │ │ │ │ │ ← endpoint (controller action)
│ │ │ │ │ │ └──┘ │ │ │ │ │ │
│ │ │ │ │ └──────┘ │ │ │ │ │
│ │ │ │ └──────────┘ │ │ │ │
│ │ │ └──────────────┘ │ │ │
│ │ └──────────────────┘ │ │
│ └──────────────────────┘ │
└────────────────────────────┘
│
▼ HTTP Response
Client
🔵 Mỗi middleware là bánh sandwich: làm gì đó trước
next, gọi next, làm gì đó sau.1.2. Program.cs chuẩn .NET 8/9
var builder = WebApplication.CreateBuilder(args);
// === 1. SERVICES (DI registration) — register ở đây ===
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt => { /* config JWT */ });
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// === 2. MIDDLEWARE PIPELINE — thứ tự QUAN TRỌNG ===
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseExceptionHandler("/error"); // 1. Catch exception trong pipeline
app.UseHttpsRedirection(); // 2. HTTP → HTTPS
app.UseStaticFiles(); // 3. Serve wwwroot/
app.UseRouting(); // 4. Match route
app.UseCors("default"); // 5. CORS check
app.UseAuthentication(); // 6. Verify token → ClaimsPrincipal
app.UseAuthorization(); // 7. Check roles/policies
app.MapControllers(); // 8. Run endpoint
app.Run();
1.3. 🟡 Custom Middleware — viết tay
public class RequestTimingMiddleware {
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(
RequestDelegate next,
ILogger<RequestTimingMiddleware> logger) {
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context) {
var sw = Stopwatch.StartNew();
// BEFORE next
_logger.LogInformation("→ {Method} {Path}",
context.Request.Method, context.Request.Path);
await _next(context); // gọi middleware tiếp theo
// AFTER next
sw.Stop();
_logger.LogInformation("← {Path} {Status} in {Ms}ms",
context.Request.Path,
context.Response.StatusCode,
sw.ElapsedMilliseconds);
}
}
// Register
app.UseMiddleware<RequestTimingMiddleware>();
1.4. 🔴 Bẫy thứ tự middleware
⭐ PV thường hỏi: thứ tự sai gây bug gì?Sai:
UseAuthorization() trước UseAuthentication() →- Authorization check role trong ClaimsPrincipal.
- Nhưng Authentication chưa populate ClaimsPrincipal.
- → User luôn unauthorized dù có token đúng.
1.5. Use vs Run vs Map
| Tác dụng | |
|---|---|
app.Use(...) | Middleware có next — chuyển control |
app.Run(...) | Terminal middleware — không gọi next |
app.Map("/path", ...) | Branch pipeline theo path |
app.MapWhen(predicate, ...) | Branch theo điều kiện |
2. Dependency Injection — sâu
2.1. 3 lifetimes
Singleton
1 instance / app
Tạo 1 lần khi register hoặc lần đầu inject. Share toàn app. Dùng cho stateless service, cache, logger, config.
Scoped
1 instance / request
Mỗi HTTP request → 1 instance mới. Dispose cuối request. Dùng cho DbContext, repository, current-user service.
Transient
Mỗi inject mới
Tạo instance mới mỗi lần inject. Stateless helper nhẹ.
builder.Services.AddSingleton<ICache, MemoryCache>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();
2.2. Vì sao DbContext nên Scoped?
🟡 DbContext = Unit of Work + Repository facade:
- Track changes của entity.
SaveChanges()= commit 1 transaction.
- Không thread-safe — không share giữa request.
- Unit of Work scope = 1 request = 1 transaction logic.
- Phải Dispose cuối request (giải phóng connection).
2.3. 🔴 Captive Dependency
⭐ Câu PV nâng cao:"Captive Dependency là gì?"Vấn đề: Inject Scoped vào Singleton → Singleton ôm Scoped đầu tiên mãi mãi.Fix: Inject
// ❌ Sai
public class WrongCache { // Singleton
private readonly AppDbContext _ctx; // Scoped
public WrongCache(AppDbContext ctx) { _ctx = ctx; }
// _ctx KHÔNG được dispose sau request đầu, dùng cho mọi request sau → bug!
}
IServiceScopeFactory thay vì DbContext trực tiếp:public class CorrectCache {
private readonly IServiceScopeFactory _scopeFactory;
public CorrectCache(IServiceScopeFactory f) => _scopeFactory = f;
public void Refresh() {
using var scope = _scopeFactory.CreateScope();
var ctx = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// dùng ctx trong scope này, dispose cuối using
}
}
2.4. Constructor injection chuẩn
public class OrderService {
private readonly IOrderRepository _repo;
private readonly INotificationService _notifier;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository repo,
INotificationService notifier,
ILogger<OrderService> logger) {
_repo = repo;
_notifier = notifier;
_logger = logger;
}
}
→ Dependency rõ ràng, immutable (readonly), test dễ (mock interface).
2.5. Đăng ký nhiều implementation
services.AddScoped<INotifier, EmailNotifier>();
services.AddScoped<INotifier, SmsNotifier>();
// Inject IEnumerable<INotifier> → nhận hết
public class AlertService {
private readonly IEnumerable<INotifier> _notifiers;
public AlertService(IEnumerable<INotifier> notifiers) => _notifiers = notifiers;
public async Task BroadcastAsync(string msg) {
foreach (var n in _notifiers) await n.SendAsync(msg);
}
}
3. REST API design
3.1. HTTP Methods + Idempotency
| Method | Mục đích | Safe? | Idempotent? | Status thường |
|---|---|---|---|---|
| GET | Đọc | ✅ | ✅ | 200, 404 |
| POST | Tạo mới | ❌ | ❌ | 201, 400 |
| PUT | Thay thế toàn bộ | ❌ | ✅ | 200, 204, 404 |
| PATCH | Cập nhật một phần | ❌ | (thường ✅) | 200, 204 |
| DELETE | Xoá | ❌ | ✅ | 204, 404 |
🟢 Idempotent = gọi nhiều lần kết quả như gọi 1 lần. Safe = không thay đổi dữ liệu (read-only).
3.2. 🟡 Vì sao PUT idempotent mà POST không?
PUT /users/123 { name: "Alice" }
→ user 123 có name = "Alice"
Gọi lại lần 2: vẫn user 123 có name = "Alice" → cùng kết quả ✅
POST /users { name: "Alice" }
→ tạo user 124 có name = "Alice"
Gọi lại: tạo user 125 → khác kết quả ❌
3.3. Status code phải thuộc
| Code | Khi nào | Ví dụ |
|---|---|---|
| 200 OK | GET, PUT có body | Trả user info |
| 201 Created | POST tạo xong | Trả 201 + Location header trỏ tới resource mới |
| 204 No Content | DELETE, PUT thành công không body | – |
| 400 Bad Request | Client gửi data sai format | Validation fail |
| 401 Unauthorized | Chưa login / sai token | – |
| 403 Forbidden | Đã login nhưng không quyền | User thường gọi endpoint admin |
| 404 Not Found | Resource không tồn tại | GET /users/9999 |
| 409 Conflict | Mâu thuẫn dữ liệu | Duplicate email khi register |
| 422 Unprocessable | Data hợp lệ syntax, vi phạm business rule | Số dư không đủ |
| 500 Server Error | Lỗi unhandled | Đừng nuốt exception trả 200! |
3.4. Controller chuẩn
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase {
private readonly IUserService _service;
public UsersController(IUserService service) => _service = service;
[HttpGet]
public async Task<ActionResult<IEnumerable<UserDto>>> GetAll() =>
Ok(await _service.GetAllAsync());
[HttpGet("{id:int}")]
public async Task<ActionResult<UserDto>> Get(int id) {
var u = await _service.GetByIdAsync(id);
return u is null ? NotFound() : Ok(u);
}
[HttpPost]
public async Task<ActionResult<UserDto>> Create(CreateUserDto dto) {
var created = await _service.CreateAsync(dto);
return CreatedAtAction(nameof(Get), new { id = created.Id }, created);
// ↑ Trả 201 + Location header + body
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, UpdateUserDto dto) {
await _service.UpdateAsync(id, dto);
return NoContent(); // 204
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id) {
await _service.DeleteAsync(id);
return NoContent();
}
}
3.5. [ApiController] magic
[ApiController]
public class UsersController : ControllerBase { ... }
[ApiController] tự động:
- Validation — nếu DTO có
[Required]không pass → tự trả 400 ProblemDetails. - Model binding — bind body/route/query thông minh.
- Attribute routing required.
- ProblemDetails cho error response (chuẩn RFC 7807).
4. JWT Authentication
4.1. JWT cấu trúc
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibmFtZSI6IkFsaWNlIn0.signature
└────── header ──────┘.└────── payload ──────┘.└─── signature ───┘
3 phần base64url-encoded, phân tách bởi .:
- Header:
{ "alg": "HS256", "typ": "JWT" }. - Payload (claims):
{ "sub": "1", "name": "Alice", "exp": 1700000000 }. - Signature: HMAC/RSA của (header + "." + payload + secret).
4.2. Setup ASP.NET Core
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt => {
opt.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
builder.Services.AddAuthorization();
// ...
app.UseAuthentication(); // PHẢI trước Authorization
app.UseAuthorization();
4.3. Sinh token
public string GenerateToken(User user) {
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Email),
new Claim(ClaimTypes.Role, user.Role)
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
4.4. 🟡 Authentication vs Authorization
⭐ PV cốt lõi:
- Authentication = "Bạn là ai?" — verify identity qua credentials / token.
- Authorization = "Bạn được làm gì?" — check role/policy/claim.
UseAuthentication() populate HttpContext.User (ClaimsPrincipal). UseAuthorization() check [Authorize(...)] dựa trên ClaimsPrincipal đó.5. Entity Framework Core
5.1. DbContext = Unit of Work + Repository facade
public class AppDbContext : DbContext {
public AppDbContext(DbContextOptions<AppDbContext> opt) : base(opt) {}
public DbSet<User> Users => Set<User>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder mb) {
mb.Entity<User>(u => {
u.HasIndex(x => x.Email).IsUnique();
u.Property(x => x.Email).HasMaxLength(150).IsRequired();
});
mb.Entity<Order>()
.HasOne(o => o.User)
.WithMany(u => u.Orders)
.HasForeignKey(o => o.UserId);
}
}
5.2. 🟡 Change tracking — cơ chế bên trong
🟡 EF Core tự theo dõi entity lấy ra qua DbContext. Mỗi entity có state:
Added: chưa save, sẽ INSERT.Modified: load rồi sửa, sẽ UPDATE.Unchanged: load rồi không sửa, không có SQL.Deleted: gọi Remove, sẽ DELETE.Detached: không track.
SaveChanges():- EF duyệt tracker, tìm entity Added/Modified/Deleted.
- Sinh SQL INSERT/UPDATE/DELETE.
- Wrap trong 1 transaction.
- Execute, update state thành Unchanged.
// Load và sửa
var user = await _db.Users.FindAsync(1);
user.Email = "new@example.com";
// state: Modified (auto track)
await _db.SaveChangesAsync();
// SQL: UPDATE Users SET Email = 'new@...' WHERE Id = 1
5.3. Tracking vs AsNoTracking
// Có tracking (default)
var users = await _db.Users.ToListAsync();
// EF lưu snapshot trong tracker → tốn RAM + CPU
// Không tracking — read-only, nhanh hơn 20–30%
var users = await _db.Users.AsNoTracking().ToListAsync();
🔵 Quy tắc: query chỉ đọc →
AsNoTracking(). Query cần update → tracking.5.4. Loading strategies — 4 cách
5.4.1. Lazy Loading
// Cần UseLazyLoadingProxies + virtual cho navigation property
public class User {
public int Id { get; set; }
public virtual ICollection<Order> Orders { get; set; } // virtual
}
var user = await _db.Users.FindAsync(1);
var orderCount = user.Orders.Count; // ← LÚC NÀY mới query lần 2
❌ Bẫy N+1: loop 100 user → 1 query users + 100 query orders.
5.4.2. Eager Loading
var users = await _db.Users
.Include(u => u.Orders)
.ThenInclude(o => o.OrderItems)
.ToListAsync();
// 1 query với JOIN (hoặc split query)
5.4.3. Explicit Loading
var user = await _db.Users.FindAsync(1);
await _db.Entry(user).Collection(u => u.Orders).LoadAsync();
// Query thứ 2 chủ động khi cần
5.4.4. Projection (tốt nhất cho API)
var dtos = await _db.Users
.Select(u => new UserDto {
Id = u.Id,
Name = u.Name,
OrderCount = u.Orders.Count
})
.ToListAsync();
// SQL: SELECT u.Id, u.Name, (SELECT COUNT(*) FROM Orders WHERE UserId = u.Id) FROM Users
// → chỉ cột cần, không tracking, nhanh nhất
5.5. 🔴 Demo N+1 problem
⭐ PV cực hay:"N+1 là gì? Cho ví dụ + fix?"Fix bằng Include:Tốt hơn — Projection:
// ❌ N+1 — 1 query users, N query orders
var users = _db.Users.ToList(); // SELECT * FROM Users (1 query)
foreach (var u in users) {
Console.WriteLine(u.Orders.Count); // SELECT * FROM Orders WHERE UserId=? (N query)
}
// → Total: 1 + N queries
var users = _db.Users.Include(u => u.Orders).ToList();
// 1 query JOIN
var data = _db.Users
.Select(u => new { u.Name, OrderCount = u.Orders.Count })
.ToList();
// 1 query, chỉ cột cần
5.6. Migration workflow
# Tạo migration mới
dotnet ef migrations add InitialCreate
# Apply lên DB
dotnet ef database update
# Sinh SQL script (production deploy)
dotnet ef migrations script > init.sql
# Rollback đến migration cụ thể
dotnet ef database update PreviousMigrationName
# Remove migration cuối (nếu chưa apply)
dotnet ef migrations remove
🟡 Production deploy migration:
- Đừng
dotnet ef database updatetrực tiếp production. - Sinh script SQL, review trước, deploy qua CI/CD.
- Test rollback trong staging.
- Big migration (alter cột bảng triệu row) → batch / online migration.
5.7. Code-First vs Database-First
| Code-First | Database-First | |
|---|---|---|
| Source of truth | C# class | Database schema |
| Workflow | Code → migration → DB | DB → scaffold → C# |
| Khi nào | Greenfield, control schema | Legacy DB |
| Lệnh | dotnet ef migrations add | dotnet ef dbcontext scaffold |
6. Câu hỏi tự test cuối ngày
🟢 Intern:
- Middleware là gì? Cho ví dụ 3 middleware có sẵn.
- 3 DI lifetime — kể tên + ý nghĩa.
- Status code: 201, 204, 401, 403, 404 — mỗi cái khi nào dùng?
UseAuthentication phải trước UseAuthorization?
5. DbContext đăng ký Scoped — vì sao?
6. IActionResult vs ActionResult<T> khác gì?
7. JWT có 3 phần — mỗi phần chứa gì?🟡 Mid:
8. Captive dependency — là gì? Fix ra sao?
9. EF Core change tracking — mô tả cơ chế.
10. N+1 problem — demo + 3 cách fix.
11. AsNoTracking() khi nào dùng, khi nào không?🔴 Senior:
12. Custom middleware vs Filter — khác gì? Khi nào dùng cái nào?
13. EF Core Find() vs FirstOrDefault() — khác cơ chế ra sao?
14. Production migration cho bảng 100M row — quy trình?
15. JWT vs Session — trade-off?Đáp án: Ngân hàng câu hỏi ASP.NET + EF.
➡️ Ngày mai: Ngày 6 — DSA coding