Thực tế, để một HTTP Request chạm được vào đúng method C#, ASP.NET Core phải kéo theo cả một hệ thống khổng lồ phía sau: Routing, Dependency Injection, Controller Factory, Model Binder, Filters, Result Executor,...
Cùng "mổ bụng" toàn bộ vòng đời của một Controller từ lúc request vừa chạm Kestrel (server của .NET) tới khi response được flush ngược ra ngoài mạng.
Cú lừa của sự tiện lợi
Có bao giờ đang debug API và nhìn vào đoạn code này thấy quen chưa?
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
private readonly IUserRepository _repository;
public UserController(IUserRepository repository)
{
_repository = repository;
}
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var user = _repository.GetById(id);
return Ok(user);
}
}
Nhìn qua, đây chỉ là một class C# bình thường.
Nhưng câu hỏi xuất hiện:
- ai là người new
UserController()? - constructor được gọi lúc nào?
- làm sao framework biết phải chạy method
GetUser()? - tại sao
return Ok()lại tự biến thành HTTP Response?
Sự tiện lợi của ASP.NET Core khiến bản thân lười tò mò. Framework che giấu gần hết phần "máy móc" phía sau bằng Attribute và "syntax sugar".
Nhưng ở tầng thấp hơn, một request phải đi qua rất nhiều công đoạn trước khi chạm vào Controller.
Toàn bộ flow có thể tóm gọn như sau:
Kestrel ↓ Middleware Pipeline ↓ Endpoint Routing ↓ Controller Factory ↓ Model Binding ↓ Filters ↓ Controller Action ↓ IActionResult ↓ Output Formatter ↓ HTTP Response
Bây giờ sẽ bóc tách từng lớp.
Tìm đường - Endpoint Routing - Giai đoạn 1
Giả sử server nhận được request: POST /api/users
Ở thời điểm này, request chỉ là một đoạn dữ liệu HTTP được Kestrel đọc từ socket mạng.
Nó hoàn toàn:
- Không biết Controller là gì
- Không biết Controller nằm ở đâu
- Càng không biết Method nào cần chạy
Khi request đi qua Middleware Pipeline, nó sẽ chạm vào một Middleware cực kì quan trọng: app.UseRouting();
Đằng sau UseRouting() là EndpointRoutingMiddleware.
Nhiệm vụ của nó:
- Đọc URL
- Đối chiếu Route Table đã build sẵn lúc app startup
- Tìm Endpoint phù hợp nhất
Ví dụ framework tra ra: /api/users → UserController.CreateUser()
Sau khi match thành công, ASP.NET Core sẽ gắn Endpoint vào HttpContext. Đây là chi tiết rất quan trọng:
Sau bước routing, framework mới chỉ biết đích "đến". Controller lúc này vẫn chưa hề được tạo ra.
Nhiều người tưởng request đi tới đâu là Controller đã tồn tại ở đó.
Không. Controller chỉ được tạo khi framework thật sự cần dùng tới nó.
Middleware và Filter không giống nhau
Đây là chỗ rất nhiều dev mới học ASP.NET Core dễ nhầm.
Middleware chạy ở tầng HTTP Pipeline toàn cục:
Request → Middleware → Middleware → Middleware
Còn Filter chỉ tồn tại bên trong MVC Pipeline sau khi request đã match route thành công. Nói đơn giản:
Middleware = bảo vệ cả tòa nhà. Filter = bảo vệ từng căn phòng
Middleware xử lý:
- Logging
- Authentication
- CORS
- Exception handling
Filter xử lý:
- Authorization cho Action
- Validate ModelState
- Before/After Action
- Result processing
Đây là hai pipeline hoàn toàn khác nhau.
Triệu hồi Controller - Controller Validation & Denpendency Injection (DI) - Giai đoạn 2
Framework giờ đã biết phải chạy: UserController.GetUser()
Nhưng có một vấn đề:
Method này không phải static. ASP.NET Core cần tạo một object UserController để gọi method đó. Đây là lúc IControllerFactory xuất hiện.
Thường framework sẽ dùng: DefaultControllerFactory. Factory này phối hợp với Dependency Injection (DI) để khởi tạo Controller.
Ví dụ constructor:
public UserController(IUserRepository repository, ILogger<UserController> logger)
{
}
Framework sẽ chạy tới DI Container và nói đại loại: "Tao cần một IUserRepository và ILogger để build UserController". Container resolve dependencies xong, framework mới tạo được instance Controller.
Một hiểu lầm phổ biến về lifetime của Controller
Nhiều người nghĩ Controller là Singleton, sống xuyên suốt ứng dụng. Hoàn toàn không phải.
Controller instance chỉ sống trong vòng đời của đúng một request.
Nếu có 1000 request cùng lúc: 1000 request -> 1000 object UserController riêng biệt.
Sau khi request kết thúc, object Controller sẽ không còn được tham chiếu nữa và Garbage Collector thu hồi vào thời điểm thích hợp. Đây là lý do không nên lưu state bên trong Controller.
Ví dụ:
public class UserController : ControllerBase
{
private static int _counter = 0;
}
State dùng chung rất dễ tạo ra race condition khi nhiều request chạy song song.
ASP.NET Core có dùng Reflection mỗi request không?
Có, nhưng không hoàn toàn theo cách nhiều người tưởng. Nếu mỗi request đều dùng Reflection toàn phần để:
- Tìm constructor
- Phân tích parameter
- Invoke method
thì hiệu năng sẽ cực kì tệ. ASP.NET Core tối ưu chuyện này bằng cách:
- Cached metadata
- Build delegate
- Compile expression tree
- Reuse ObjectFactory
Nói cách khác: Reflection chủ yếu dùng lúc "chuẩn bị". Khi request thật chạy, framework cố chuyển mọi thứ sang delegate để tăng tốc. Đây là một trong những lý do ASP.NET Core nhanh hơn nhiều so với framework cũ.
Nhồi nhét dữ liệu - Model Binding
Controller đã được tạo xong. Giờ tới vấn đề tiếp theo: Làm sao đoạn JSON này:
{
"name": "Duy",
"age": 29
}
lại biến thành object C#:
public class UserDto
{
public string Name { get; set; }
public int Age { get; set; }
}
Đây là nhiệm vụ của Model Binding. ASP.NET Core sẽ cử một loạt IModelBinder ra xử lý.
Binder lấy dữ liệu từ đâu?
Tùy attribute:
[FromRoute]: [HttpGet("{id}")] publicIActionResult Get(int id)- Framework cắt URL để lấy id.[FromQuery]: GET/users?page=2- Framework parse query string.[FromHeader]: Đọc từ HTTP Header.[FromBody]: Đây là phần thú vị nhất.
Với [FromBody], Framework sẽ:
- đọc Request Body Stream
- gọi Input Formatter
- deserialize JSON thành object C#
Mặc định ASP.NET Core thường dùng: System.Text.Json
Cái bẫy Stream: Request Body chỉ đọc được một lần
Đây là bug rất nhiều người từng dính. Request Body thực chất là một Stream. Mà Stream thì:
- Đọc tới đâu trôi tới đó
- Không tự rewind
Nếu Middleware đọc body trước:
using var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync();
thì Model Binder phía sau có thể đọc trúng stream rỗng. Kết quả: input == null dù client gửi JSON hoàn toàn hợp lệ.
Muốn đọc nhiều lần phải bật buffering: context.Request.EnableBuffering();
Đây là kiểu bug nhìn rất “tâm linh” nếu không hiểu pipeline bên dưới.
Filter Pipeline - Giai đoạn 4
Sau khi Model Binding hoàn tất, request chưa vào Controller ngay. Nó còn phải đi qua Filter Pipeline.
Ví dụ:
Authorization Filter
↓
Action Filter
↓
Action Method
↓
Result Filter
Đây là nơi ASP.NET Core xử lý:
- Authorization
- Validation
- Before/after action
- Modify result
Ví dụ ActionFilter:
public class LogFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
Console.WriteLine("Trước khi Action chạy");
}
public override void OnActionExecuted(ActionExecutedContext context)
{
Console.WriteLine("Sau khi Action chạy");
}
}
Nhìn quen không? Nó rất giống Middleware. Khác biệt là:
- Middleware hoạt động ở tầng HTTP toàn cục
- Filter hoạt động bên trong MVC Pipeline
Action Invocation - Thực thi method - Giai đoạn 5
Cuối cùng, framework mới chính thức gọi: GetUser(id) Thông qua IActionInvoker.
Đây là khoảnh khắc business logic thật sự chạy. Database query, cache, call external API, tính toán... mọi thứ xảy ra ở đây.
IActionResult không phải HTTP Response
Đây là chỗ rất nhiều dev hiểu nhầm. Khi viết: return Ok(user); đa số nghĩ: “Data được trả về ngay cho client.” Thực tế chưa có byte nào được gửi đi cả. Ok(user) chỉ tạo ra: OkObjectResult.
Đây chỉ là một object mô tả:
- Status code = 200
- Body = user
Nó giống một tờ chỉ thị hơn là response thật.
Result Execution
Sau khi Action kết thúc, framework mới bắt đầu: Result Execution.
Lúc này:
- Output Formatter serialize object thành JSON
- Header được ghi
- Status Code được set
- Dữ liệu được ghi vào Response Stream.
Cuối cùng ASP.NET Core mới flush stream trả về cho client. Ví dụ object:
new UserDto
{
Name = "Duy"
}
sẽ được serialize thành:
{
"name": "Duy"
}
rồi mới bắn ngược ra network socket.
Kết luận
Nhìn bề ngoài, Controller chỉ là vài dòng code rất “hiền”: return Ok(data);. Nhưng để chạy được dòng code đó, ASP.NET Core phải kéo theo cả một hệ thống khổng lồ:
- Routing tìm đường
- Factory tạo object
- DI resolve dependencies
- Model Binder nhét dữ liệu
- Filters xử lý pipeline
- Invoker execute method
- Formatter serialize response
Thứ trông như “phép thuật” thực ra chỉ là rất nhiều component nhỏ phối hợp với nhau cực kỳ bài bản. Hiểu luồng chạy này không giúp bạn code CRUD nhanh hơn. Nhưng nó cho bạn một thứ mạnh hơn nhiều: Khả năng debug framework bằng tư duy hệ thống.
Khi API tự nhiên:
- Trả 404
- Bind param bị null
- Response serialize sai
- Filter không chạy
- Middleware nuốt mất body
Lúc này sẽ biết chính xác request đang “kẹt” ở tầng nào thay vì debug trong tuyệt vọng. ASP.NET Core không thần bí. Nó chỉ là một pipeline được thiết kế cực kỳ thông minh.
No comments yet. Be the first to comment!