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ướcnext, 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.
Đúng: Routing → CORS → Authentication → Authorization → Endpoints.

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.
Phải Scoped vì:
  1. Không thread-safe — không share giữa request.
  2. Unit of Work scope = 1 request = 1 transaction logic.
  3. 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.
// ❌ 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!
}
Fix: Inject 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

MethodMục đíchSafe?Idempotent?Status thường
GETĐọc200, 404
POSTTạo mới201, 400
PUTThay thế toàn bộ200, 204, 404
PATCHCập nhật một phần(thường ✅)200, 204
DELETEXoá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

CodeKhi nàoVí dụ
200 OKGET, PUT có bodyTrả user info
201 CreatedPOST tạo xongTrả 201 + Location header trỏ tới resource mới
204 No ContentDELETE, PUT thành công không body
400 Bad RequestClient gửi data sai formatValidation fail
401 UnauthorizedChưa login / sai token
403 ForbiddenĐã login nhưng không quyềnUser thường gọi endpoint admin
404 Not FoundResource không tồn tạiGET /users/9999
409 ConflictMâu thuẫn dữ liệuDuplicate email khi register
422 UnprocessableData hợp lệ syntax, vi phạm business ruleSố dư không đủ
500 Server ErrorLỗ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.
Trong middleware: 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.
Khi SaveChanges():
  1. EF duyệt tracker, tìm entity Added/Modified/Deleted.
  2. Sinh SQL INSERT/UPDATE/DELETE.
  3. Wrap trong 1 transaction.
  4. 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?"
// ❌ 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
Fix bằng Include:
var users = _db.Users.Include(u => u.Orders).ToList();
// 1 query JOIN
Tốt hơn — Projection:
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:
  1. Đừng dotnet ef database update trực tiếp production.
  2. Sinh script SQL, review trước, deploy qua CI/CD.
  3. Test rollback trong staging.
  4. Big migration (alter cột bảng triệu row) → batch / online migration.

5.7. Code-First vs Database-First

Code-FirstDatabase-First
Source of truthC# classDatabase schema
WorkflowCode → migration → DBDB → scaffold → C#
Khi nàoGreenfield, control schemaLegacy DB
Lệnhdotnet ef migrations adddotnet ef dbcontext scaffold

6. Câu hỏi tự test cuối ngày

🟢 Intern:
  1. Middleware là gì? Cho ví dụ 3 middleware có sẵn.
  2. 3 DI lifetime — kể tên + ý nghĩa.
  3. Status code: 201, 204, 401, 403, 404 — mỗi cái khi nào dùng?
🔵 Junior: 4. Vì sao 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

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.