Sự giống nhau nhưng lại khác nhau

Nếu nhìn qua, Middleware và Filter rất giống nhau.

Ví dụ:

Middleware:

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;

    public LoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        Console.WriteLine("Before");

        await _next(context);

        Console.WriteLine("After");
    }
}

Filter:

public class LoggingFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("Before");
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine("After");
    }
}

Nó giống nhau:

  • Đều xử lý before/after
  • Đều log được request
  • Đều có thể chặn request
  • Đều có thể xử lý exception

Chính vì quá giống nhau, nên nhiều người bắt đầu nghĩ:

Filter chính là Middleware dành cho Controller.

Không hẳn.

Thực tế Middleware và Filter là hai pipeline hoàn toàn khác nhau.

Middleware - tầng bảo vệ toàn cục

Middleware hoạt động ở tầng HTTP Pipeline.

Nói đơn giản:

Mọi HTTP Request đi vào server, đều đi qua Middleware trước.

Kể cả request đó:

  • API
  • Swagger
  • Static File
  • Image
  • CSS
  • Javascript

Flow sẽ như thế này:

HTTP Request 
    ↓ 
Middleware 
    ↓ 
Middleware 
    ↓
Middleware 
    ↓ 
Endpoint

Đây là lý do Middleware thường được dùng cho:

  • Authentication
  • Logging
  • Exception handling
  • CORS
  • Rate limiting

Vì nó hoạt động ở tầng toàn cục.

await _next(contex) thật ra đang làm gì?

Ví dụ:

await _next(context);

Nghĩa là:

Cho request đi tiếp xuống Middleware tiếp theo

Nếu không gọi _next(context):

public async Task InvokeAsync(HttpContext context) 
{ 
    context.Response.StatusCode = 401; 
    await context.Response.WriteAsync("Unauthorized"); 
}

Pipeline sẽ dừng ngay tại middleware đó. Request không bao giờ tới Controller

Đây gọi là Short-circuit pipeline. Rất nhiều middleware hoạt động theo kiểu:

  • Authentication fail
  • Rate limit vượt quá giới hạn
  • Request invalid
  • Cache hit

Lúc này sẽ response luôn, không đi tiếp nữa.

Controller chưa phải nơi request đi vào đầu tiên

Đây là hiểu lầm rất phổ biến. Nhiều người nghĩ request đi thẳng vào Controller.

Không.

Trước khi chạm vào Controller, request đã và phải đi qua rất nhiều tầng bên dưới.

Flow thật sự đã được nói ở bài viết "Controller hoạt động thế nào?"

HTTP Request 
    ↓ 
Middleware Pipeline 
    ↓ 
Routing 
    ↓ 
Authentication 
    ↓ 
Authorization 
    ↓ 
MVC Pipeline 
    ↓ 
Filters 
    ↓ 
Controller Action

Filter chỉ xuất hiện khi và chỉ khi request đã:

  • Match route thành công
  • Xác định được Controller/Action cần chạy

Nếu request không match route:

404 Not Found

Lúc này, Filter thậm chí còn chưa tồn tại. Trong khi Middleware vẫn chạy bình thường.

Middleware và Filter khác nhau ở đâu?

Nói đơn giản:

Middleware xử lý ở tầng HTTP toàn cục. Filter xử lý bên trong MVC Pipeline.

Ví dụ:

Middleware giống như bảo vệ ngoài cổng của toà nhà. Filter sẽ giống bảo vệ riêng cho từng phòng trong toà nhà đó.

Middleware sẽ không biết:

  • Action nào sắp chạy
  • Model gì được bind
  • Controller nào được gọi

Nó chỉ biết:

Có một HTTP Request vừa đi vào server

Ngược lại, Filter lúc này đã biết rất nhiều thứ:

  • Controller nào đang chạy
  • Action nào được gọi
  • Parameter truyền vào là gì
  • Authorization theo Action
  • Before/After Action

Vì sao Middleware chạy nhưng Filter không chạy?

Đây là bug kinh điển chắc nhiều người đã từng gặp.

Ví dụ:

Request 
    ↓ 
Middleware chạy 
    ↓ 
Routing fail 
    ↓ 
404 Not Found

Lúc này:

  • Middleware vẫn execute bình thường
  • Nhưng Filter không hề chạy

Lý do: vì request chưa bao giờ đi được và vào tới MVC Pipeline.

Đây cũng là lý do đôi khi debug mới thấy:

Ủa sao middleware log được mà filter không chạy?

Exception Middleware và Exception Filter không giống nhau

Exception Middleware:

Bắt exception ở tầng HTTP Pipeline

Exception Filter:

Bắt exception bên trong MVC Pipeline

Ví dụ exception xảy ra trước khi vào MVC, lúc này Authentication Middleware nổ exception thì Exception Filter hoàn toàn không bắt được.

Ngược lại, nếu exception xảy ra bên trong Controller Action:

Controller 
    ↓ 
throw Exception

Lúc này, Exception Filter mới có cơ hội xử lý.

Bẫy khi đặt logic sai tầng

Ví dụ:

  • Validate JWT trong Filter
  • Log request body trong Controller
  • Exception handling bằng ActionFilter
  • Cache logic nằm trong Action

Mọi thứ vẫn chạy được. Nhưng architecture sẽ bắt đầu rối.

Và một quy tắc ra đời:

  • Logic toàn cục -> Middleware
  • Logic liên quan MVC/Action -> Filter

Kết luận

Middleware và Filter nhìn khá giống nhau.

Cả hai đều:

  • Log
  • Validate
  • Bắt exception
  • Xử lý before/after

Nhưng thực tế, nó nằm ở hai pipeline hoàn toàn khác nhau bên trong ASP.NET Core

  • Middleware hoạt động ở tầng HTTP toàn cục
  • Filter chỉ tồn tại sau khi request đi vào MVC Pipeline

Hiểu được flow này sẽ giúp xác định chính xác:

  • Request đang kẹt ở đâu
  • Logic nên đặt tầng nào
  • Vì sao Filter không chạy
  • Vì sao Middleware không bắt được exception
  • Vì sao request chưa chạm được Controller

Middleware và Filter không chỉ giúp nhau, tương tác với nhau và đang phối hợp cùng nhau phía dưới một HTTP Request.