Middleware trong ASP.NET Core

2022-08-25 15:04:19 | khoảng 18 phút đọc

Xin chào các bạn, mình là Huy, một developer. Bài viết này chúng ta sẽ cũng nhau tìm hiểu về cách mà middleware hoạt động cũng như các tính năng vượt trội mà ASP.NET Core mang lại cho chúng ta.

Middleware là phần mềm được tập hợp thành một pipeline1 để xử lý các request và response đi qua nó. Mỗi thành phần middleware:

  • Quyết định có chuyển request đến thành phần middleware tiếp theo hay không
  • Có thể thực hiện một số công việc trước và sau thành phần middleware tiếp theo trong pipeline.

Các request delegate được sử dụng để xây dựng request pipeline, chúng xử lý từng HTTP request.

Các request delegate được cấu hình sử dụng các extension method như Run, MapUse. Một request delegate có thể được chỉ định in-line như anonymous method (gọi là in-line middleware) hoặc nó có thể định nghĩa trong một class có khả năng tái sử dụng. Các class tái sử dụng và in-line anonymous method đều là middleware, còn được gọi là các thành phần middleware. Mỗi thành phần middleware trong request pipeline có trách nhiệm gọi thành phần tiếp theo hoặc làm ngắn mạch pipeline. Khi middleware ngắn mạch, nó được gọi là terminal middleware bởi vì nó ngăn middleare tiếp tục xử lý yêu cầu.

Phân tích middleware code

ASP.NET Core bao gồm nhiều trình phân tích nền tảng biên dịch để kiểm tra chất lượng của code ứng dụng. Các code này giúp cho chúng ta biết các lỗi mà middleware gặp phải để dễ dàng sửa lỗi. Để biết thêm thông tin chi tiết, xem tại Code analysis in ASP.NET Core apps.

Tạo một middleware pipeline với WebApplication

ASP.NET Core request pipeline bao gồm một chuỗi các request delegate, được gọi lần lượt. Sơ đồ bên dưới cho chúng ta biết các hoạt động của một vòng đời request pipeline, hướng theo mũi tên màu đen.

Sơ đồ hoạt động middleware request pipline trong ASP.NET Core

Mỗi delegate có thể thực hiện các đoạn code trước và sau delegate tiếp theo. Trình xử lý exception của delegate nên được gọi sớm trong pipeline để chúng có thể catch các exception xuất hiện ở giai đoạn sau của pipeline.

Một ứng dụng ASP.NET Core đơn giản nhất thiết lập một request delegate để xử lý tất cả request. Trường hợp này không bao gồm một request pipeline thực tế. Thay vào đó, một anonymous function duy nhất được gọi trong response đến mọi HTTP request.

Đầu tiên chúng ta khởi tạo dự án ASP.NET Core web app với tên là middleware.

dotnet new webapp -o middleware

Để thử tạo một request delegate để xử lý request, ta mở file Program.cs và chèn đoạn code sau:

var app = builder.Build();
 
+app.Run(async context =>
+{
+ await context.Response.WriteAsync("Hello world!");
+});
 
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())

Sau đó chúng ta build ứng dụng và truy cập vào địa chỉ https://localhost:{PORT} để xem kết quả.

Lúc này trình duyệt sẽ hiển thị dòng chữ "Hello world!". Dù chúng ta có truy cập bất cứ URI nào đó chăng nữa, chẳng hạn https://localhost:{PORT}/home, thì kết quả vẫn trả về "Hello world". Lý do bởi vì trước khi ứng dụng khởi động, nó đã chạy đoạn code delegate trên và thực thi lệnh response trước.

Để có thể nối nhiều delegate lại với nhau, ta sử dụng Use. Tham số next đại diện cho delegate kế tiếp trong pipeline. Chúng ta có thể ngắn mạch pipeline bằng cách không gọi tham số next này. Bạn có thể thực hiện một số hành động trước và sau next delegate, cụ thể như ví dụ ở bên dưới trong file Program.cs:

var app = builder.Build();
 
-app.Run(async context =>
-{
- await context.Response.WriteAsync("Hello world!");
-});
 
+app.Use(async (context, next) =>
+{
+ // Do work that can write to the Response.
+ await next.Invoke();
+ // Do logging or other work that doesn't write to the Response.
+});
+ 
+app.Run(async context =>
+{
+ await context.Response.WriteAsync("Hello from 2nd delegate.");
+});
 
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())

Khi một delegate không thể chuyển một request đến delegate tiếp theo. Nó gọi là ngắn mạch request pipeline. Việc ngắn mạch thường được mong muốn vì nó tránh được một số công việc không cần thiết.

Các Run delegate không nhận tham số next. Run delegate đầu tiên luôn là terminal và kết thúc pipeline. Run là một quy ước. Một số middleware có thể gọi phương thức Run[Middleware] để chạy cuối pipeline.

Trong đoạn code trên, Run delegate sẽ trả về "Hello from 2nd delegate." từ response và sau đó kết thúc pipeline. Nếu có một Use hoặc Run khác được thêm ở bên dưới Run delegate này, chúng sẽ không được gọi.

Source code: Tạo một middleware pipeline với WebApplication

Thứ tự middleware

Sơ đồ bên dưới hiển thị quy trình xử lý yêu cầu hoàn chỉnh của các ứng dụng ASP.NET Core MVC và Razor pages. Như các bạn nhìn thấy, trong một ứng dụng thông thường, các middleware có sẵn được sắp xếp theo thứ tự và nơi nào là nơi đặt các middleware tùy chỉnh của chúng ta. Bạn hoàn toàn có quyền sắp xếp lại vị trí các middleware có sẵn hoặc thêm vài middleware tùy chỉnh nếu như cần thiết cho ngữ cảnh.

Thứ tự middleware trong ASP.NET Core

Endpoint middleware trong sơ đồ trên thực thi filter pipeline cho các loại ứng dụng tương ứng, MVC hoặc Razor Pages.

Endpoint middleware cho ứng dụng MVC và Razor Pages trong ASP.NET Core

Routing middleware trong sơ đồ trên được hiển thị sau Static Files. Đây là thứ tự mà các project template thực hiện bằng cách gọi cụ thể phương thức app.UseRouting. Nếu ta không gọi app.UseRouting, Routing middleware sẽ được chạy đầu trong pipeline theo mặc định.

Thứ tự các thành phần middleware được thêm vào trong file Program.cs chính là thứ tự mà các thành phần middleware thực thi trên request và ngược lại đối với response. Thứ tự này rất quan trọng cho bảo mật, hiệu suất và tính năng.

Đoạn code được highlight bên dưới thêm các thành phần middleware liên quan đến bảo mật theo thứ tự được khuyến khích:

var app = builder.Build();
 
// Configure the HTTP request pipeline. //
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
 
app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();
 
app.UseRouting();
// app.UseRequestLocalization();
// app.UseCors();
 
app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
 
app.MapRazorPages();
 
app.Run();

Không phải mọi middleware phải xuất hiện theo thứ tự chính xác, nhưng một số thì bắt buộc. Ví dụ:

  • UseCors, UseAuthenticationUseAuthorization phải xuất hiện theo thứ tự hiển thị.
  • UseCors thường phải xuất hiện trước UseResponseCaching. Lý do này được giải thích tại GitHub issue dotnet/aspnetcore #23218.
  • UseRequestLocalization phải xuất hiện trước bất kỳ middleware nào cần kiểu tra request culture, chẳng hạn như app.UseMvcWithDefaultRoute().

Trong một số trường hợp, middleware có thứ tự sắp xếp khác nhau. Ví dụ như thứ tự caching và trình nén là trường hợp đặc biệt, có nhiều cách sắp xếp hợp lệ. Ví dụ:

app.UseResponseCaching();
app.UseResponseCompression();

Với cách sắp xếp trên, CPU sẽ được giảm thiểu bằng cách cache compressed response.

Thứ tự bên dưới kết hợp với static file cho phép caching các static file đã được nén.

app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();

Đoạn code ví dụ bên dưới thêm các thành phần middleware cho các ngữ cảnh ứng dụng phổ biến:

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();

Lần lượt xét từ trên xuống:

  1. Trình xử lý exception/lỗi:
    • Khi ứng dụng chạy trong môi trường Development:
      • Developer Exception Page Middleware (UseDeveloperExceptionPage) báo cáo lỗi runtime của ứng dụng.
      • Database Error Page Middleware (UseDatabaseErrorPage) báo cáo lỗi runtime của database.
    • Khi ứng dụng chạy trong môi trường Production:
      • Exception Handler Middleware (UseExceptionHandler) catch các exception được throw trong middleware.
  2. HTTPS Redirection Middleware (UseHttpsRedirection) chuyển hướng HTTP sang HTTPS.
  3. Static File Middleware (UseStaticFiles) trả về các static file và làm ngắn mạch request.
  4. Cookie Policy Middleware (UseCookiePolicy) tuân thủ ứng dụng với các quy định EU General Data Protection Regulation (GDPR).
  5. Routing Middleware (UseRouting) dùng để định tuyến request.
  6. Authentication Middleware (UseAuthentication) cố gắng xác thực người dùng trước khi họ truy cập vào các tài nguyên bảo mật.
  7. Authorization Middleware (UseAuthorization) ủy quyền người dùng để truy cập vào các tài nguyên bảo mật.
  8. Session Middleware (UseSession) thiết lập và duy trì trạng thái session. Nếu một ứng dụng sử dụng trạng thái session, gọi Session Middleware sau Cookie Policy Middleware và trước MVC Middleware.
  9. Endpoint Routing Middleware (UseEndpoints với MapRazorPages) dùng để thêm các Razor page endpoint đến request pipeline.

Trong đoạn code trên, mỗi middleware extension method được gọi trên WebApplicationBuilder thông qua namespace Microsoft.AspNetCore.Builder.

UseExceptionHandler là middleware đầu tiên được thêm vào pipeline. Do đó, Exception Handler Middleware bắt được bất kỳ những exception nào xuất hiện ở những lần gọi sau.

Static File middleware được gọi sớm trong pipeline để nó có thể xử lý request và ngắn mạch mà không cần đi qua các thành phần còn lại. Static File Middleware không cần kiểm tra ủy quyền. Bất kỳ tệp nào được cung cấp bởi Static File middleware, bao gồm các tệp trong wwwroot, đều là công khai. Để tiếp cận các static file an toàn, chúng ta sẽ tìm hiểu ở các bài học sau.

Nếu request không được xử lý bởi Static File middleware, nó sẽ được chuyển đến Authentication Middleware (UseAuthentication) để thực hiện xác thực người dùng. Authentication middleware không làm ngắn mạch các request chưa được xác thực. Mặc dù Authentication Middleware xác thực request, việc ủy quyền (và từ chối) chỉ xảy ra khi MVC chọn một Razor Page cụ thể, hoặc MVC controller và action.

Ví dụ sau minh họa một thứ tự sắp xếp middleware trong đó yêu cầu các static file được xử lý bởi Static File Middleware trước Response Compression Middleware. Các static file không thể compress với thứ tự sắp xếp này. Những Razor Pages response có thể compress.

// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();
 
app.UseRouting();
 
app.UseResponseCompression();
 
app.MapRazorPages();

Phân nhánh middleware pipeline

Các Map extension được dùng như một quy ước cho việc phân nhánh pipeline. Map phân nhánh request pipeline dựa trên các kết quả phù hợp của request path cho trước. Nếu request path bắt đầu với một path đã cho trước, nhánh sẽ được thực thi.

Quay trở lại dự án khi nãy, chúng ta thay đổi nội dung file Program.cs từ dòng 6 như sau:

6var app = builder.Build();
7 
8app.Map("/map1", HandleMapTest1);
9app.Map("/map2", HandleMapTest2);
10 
11app.Run(async context =>
12{
13 await context.Response.WriteAsync("Hello from non-Map delegate.");
14});
15 
16static void HandleMapTest1(IApplicationBuilder app)
17{
18 app.Run(async context =>
19 {
20 await context.Response.WriteAsync("Map Test 1");
21 });
22}
23 
24static void HandleMapTest2(IApplicationBuilder app)
25{
26 app.Run(async context =>
27 {
28 await context.Response.WriteAsync("Map Test 2");
29 });
30}
31 
32// Configure the HTTP request pipeline.
33if (!app.Environment.IsDevelopment())

Bảng bên dưới thể hiện những request và response tương ứng từ https://localhost:{PORT} sử dụng đoạn code trên:

Request Response
localhost:{PORT} Hello from non-Map delegate.
localhost:{PORT}/map1 Map Test 1
localhost:{PORT}/map2 Map Test 2
localhost:{PORT}/map3 Hello from non-Map delegate.

Khi Map được sử dụng, các path segment phù hợp sẽ bị gỡ bỏ từ HttpRequest.Path và thêm vào HttpRequest.PathBase cho mỗi request.

Map có hỗ trợ nesting:

-app.Map("/map1", HandleMapTest1);
-app.Map("/map2", HandleMapTest2);
 
+app.Map("/level", levelApp => {
+ levelApp.Map("/map1", HandleMapTest1);
+ 
+ levelApp.Map("/map2", HandleMapTest2);
+});

Bảng bên dưới thể hiện những request và response tương ứng từ https://localhost:{PORT} sử dụng đoạn code trên:

Request Response
localhost:{PORT} Hello from non-Map delegate.
localhost:{PORT}/level/map1 Map Test 1
localhost:{PORT}/level/map2 Map Test 2
localhost:{PORT}/level/map3
localhost:{PORT}/map3 Hello from non-Map delegate.

Map cũng hỗ trợ kiểm nhà nhiều segment một lúc:

-app.Map("/level", levelApp => {
- levelApp.Map("/map1", HandleMapTest1);
- 
- levelApp.Map("/map2", HandleMapTest2);
-});
 
+app.Map("/map1/seg1", HandleMapTest1);

Bảng bên dưới thể hiện những request và response tương ứng từ https://localhost:{PORT} sử dụng đoạn code trên:

Request Response
localhost:{PORT} Hello from non-Map delegate.
localhost:{PORT}/map1/seg1 Map Test 1
localhost:{PORT}/map3 Hello from non-Map delegate.

Ngoài ra còn có MapWhen, phân nhánh request pipeline dựa trên kết quả của vị từ2 cho trước. Bất kỳ vị từ nào thuộc kiểu Func<HttpContext, bool> có thể sử dụng để ánh xạ các request đến tới một nhánh mới của pipeline. Trong ví dụ bên dưới, một vị từ dùng để xác định sự hiện diện của query string branch:

+app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);
 
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
 
+static void HandleBranch(IApplicationBuilder app)
+{
+ app.Run(async context =>
+ {
+ var branchVer = context.Request.Query["branch"];
+ await context.Response.WriteAsync($"Branch used = {branchVer}");
+ });
+}

Bảng bên dưới thể hiện những request và response tương ứng từ https://localhost:{PORT} sử dụng đoạn code trên:

Request Response
localhost:{PORT} Hello from non-Map delegate.
localhost:{PORT}/?branch=main Branch used = main

UseWhen cũng phân nhánh request pipeline dựa trên kết quả của vị từ cho trước. Không như MapWhen, nhánh này có thể tham gia lại vào trong main pipeline nếu nó không ngắn mạch hoặc chứa một terminate middleware:

-app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);
 
+app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
+ appBuilder => HandleBranchAndRejoin(appBuilder));
 
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
 
+void HandleBranchAndRejoin(IApplicationBuilder app)
+{
+ var logger = app.ApplicationServices.GetRequiredService<ILogger<Program>>();
+ 
+ app.Use(async (context, next) =>
+ {
+ var branchVer = context.Request.Query["branch"];
+ logger.LogInformation("Branch used = {branchVer}", branchVer);
+ 
+ // Do work that doesn't write to the Response.
+ await next();
+ // Do other work that doesn't write to the Response.
+ });
+}

Trong ví dụ trên, nó được viết cho tất cả request chứa query string branch. Do nhánh trên không ngắn mạch và chứa bất kỳ terminate middleware nào nên khi truy cập vào https://localhost:{PORT}?branch=main ta nhận được response với nội dung "Hello from non-Map delegate." nhưng trong console terminal sẽ hiển thị một message như bên dưới:

Branch used = main

Source code: Phân nhánh middleware pipeline

Middleware tích hợp sẵn trong ASP.NET Core

Các bạn có thể tham khảo thêm tại Built-in middleware.

Tổng kết

Qua bài này chúng ta đã hiểu middleware là gì, cũng như là cách nó hoạt động trong ASP.NET Core. Dù là cùng tập hợp middleware đó nhưng chỉ cần đổi thứ tự sắp xếp cũng có thể cho ra kết quả, ngữ nghĩa khác nhau. Hi vọng bài việt này giúp ích được cho mọi người. Cảm ơn các bạn đã quan tâm theo dõi, hẹn gặp lại!


  1. Nói một cách dễ hiểu thì các middleware sẽ được sắp xếp với nhau theo thứ tự, tạo thành một đường ống dẫn thì gọi là pipeline. Việc ngắn mạch này thường được mong đợi vì tránh một số công việc không cần thiết. 

  2. Vị từ có thể xem là một hàm mệnh đề có nhiều biến hoặc không có biến nào, nó có thểđúng hoặc sai tùy thuộc vào giá trị của biến và lập luận của vị từ. 

search
Bài viết
Series
Thẻ

Không tìm thấy kết quả nào.