使用Visual Studio Code開發Asp.Net Core WebApi學習筆記(四)-- Middleware


本文記錄了Asp.Net管道模型和Asp.Net Core的Middleware模型的對比,並在上一篇的基礎上增加Middleware功能支持。

在演示Middleware功能之前,先要了解一下Asp.Net管道模型發生了什么樣的變化。

第一部分:管道模型

1. Asp.Net管道

在之前的Asp.Net里,主要的管道模型流程如下圖所示:

請求進入Asp.Net工作進程后,由進程創建HttpWorkRequest對象,封裝此次請求有關的所有信息,然后進入HttpRuntime類進行進一步處理。HttpRuntime通過請求信息創建HttpContext上下文對象,此對象將貫穿整個管道,直到響應結束。同時創建或從應用程序池里初始化一個HttpApplication對象,由此對象開始處理之前注冊的多個HttpModule。之后調用HandlerFactory創建Handler處理程序,最終處理此次請求內容,生成響應返回。

下面用一個簡單的Asp.Net程序來驗證這個流程。

使用VS2015創建一個空的Asp.Net項目,根據向導添加HttpModule.cs、HttpHandler.cs、Global.asax文件

 1 using System.Web;
 2 
 3 namespace WebApplicationTest
 4 {
 5     public class HttpModule1 : IHttpModule
 6     {
 7         public void Dispose()
 8         {
 9 
10         }
11 
12         public void Init(HttpApplication context)
13         {
14             context.BeginRequest += (sender, e) =>
15             {
16                 context.Response.Write("HttpModule1 request begin....<br />");
17             };
18 
19             context.EndRequest += (sender, e) =>
20             {
21                 context.Response.Write("HttpModule1 request end!<br />");
22             };
23         }
24     }
25 
26     public class HttpModule2 : IHttpModule
27     {
28         public void Dispose()
29         {
30 
31         }
32 
33         public void Init(HttpApplication context)
34         {
35             context.BeginRequest += (sender, e) =>
36             {
37                 context.Response.Write("HttpModule2 request begin....<br />");
38             };
39 
40             context.EndRequest += (sender, e) =>
41             {
42                 context.Response.Write("HttpModule2 request end!<br />");
43             };
44         }
45     }
46 
47     public class HttpModule3 : IHttpModule
48     {
49         public void Dispose()
50         {
51 
52         }
53 
54         public void Init(HttpApplication context)
55         {
56             context.BeginRequest += (sender, e) =>
57             {
58                 context.Response.Write("HttpModule3 request begin....<br />");
59             };
60 
61             context.EndRequest += (sender, e) =>
62             {
63                 context.Response.Write("HttpModule3 request end!<br />");
64             };
65         }
66     }
67 }
HttpModule.cs
 1 using System.Web;
 2 
 3 namespace WebApplicationTest
 4 {
 5     public class HttpHandler : IHttpHandler
 6     {
 7         public bool IsReusable
 8         {
 9             get
10             {
11                 return true;
12             }
13         }
14 
15         public void ProcessRequest(HttpContext context)
16         {
17             context.Response.ContentType = "text/html";
18             context.Response.Write("Hello world!<br />");
19             context.Response.End();
20         }
21     }
22 }
HttpHandler.cs

配置Web.Config。以下是在IIS7環境下的配置內容。

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <!--
 3   有關如何配置 ASP.NET 應用程序的詳細信息,請訪問
 4   http://go.microsoft.com/fwlink/?LinkId=169433
 5   -->
 6 <configuration>
 7   <system.web>
 8     <compilation debug="true" targetFramework="4.5"/>
 9     <httpRuntime targetFramework="4.5"/>
10   </system.web>
11   <system.webServer>
12     <validation validateIntegratedModeConfiguration="false"/>
13     <handlers>
14       <add name="handler" verb="GET" path="index.handler" type="WebApplicationTest.HttpHandler,WebApplicationTest"/>
15     </handlers>
16     <modules>
17       <add name="module1" type="WebApplicationTest.HttpModule1,WebApplicationTest"/>
18       <add name="module2" type="WebApplicationTest.HttpModule2,WebApplicationTest"/>
19       <add name="module3" type="WebApplicationTest.HttpModule3,WebApplicationTest"/>
20     </modules>
21   </system.webServer>
22 </configuration>

啟動調試,訪問地址 http://localhost:5383/index.handler ,可以看到頁面內容。

之前版本的Asp.Net MVC正是通過 UrlRoutingModule.cs 類和 MvcHandler.cs 類進行擴展從而實現了MVC框架。

2、Asp.Net Core管道

而在Asp.Net Core里面,管道模型流程發生了很大的變化:

IHttpModule和IHttpHandler不復存在,取而代之的是一個個中間件(Middleware)。

Server將接收到的請求直接向后傳遞,依次經過每一個中間件進行處理,然后由最后一個中間件處理並生成響應內容后回傳,再反向依次經過每個中間件,直到由Server發送出去。

中間件就像一層一層的“濾網”,過濾所有的請求和相應。這一設計非常適用於“請求-響應”這樣的場景——消息從管道頭流入最后反向流出。

接下來將演示在Asp.Net Core里如何實現中間件功能。

 

第二部分、Middleware

其實,在這個系列的第一篇里面,已經展示了管道的一個簡單用法。這里再詳細講解一下如何實現自定義管道。

Middleware支持Run、Use和Map三種方法進行注冊,下面將展示每一種方法的使用方式。

一、Run方法

所有需要實現的自定義管道都要在 Startup.cs 的 Configure 方法里添加注冊。

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
 2         {
 3             // 添加日志支持
 4             loggerFactory.AddConsole();
 5             loggerFactory.AddDebug();
 6             
 7             // 添加NLog日志支持
 8             loggerFactory.AddNLog();
 9 
10             // 添加自定義中間件
11             app.Run(async context =>
12             {
13                 await context.Response.WriteAsync("Hello World!");
14             });
15 
16             // 添加MVC中間件
17             //app.UseMvc();
18         }

啟動調試,訪問地址 http://localhost:5000/ ,頁面顯示Hello World!字樣。

再次添加一個Run方法

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
 2         {
 3             // 添加日志支持
 4             loggerFactory.AddConsole();
 5             loggerFactory.AddDebug();
 6             
 7             // 添加NLog日志支持
 8             loggerFactory.AddNLog();
 9 
10             // 添加自定義中間件
11             app.Run(async context =>
12             {
13                 await context.Response.WriteAsync("Hello World!");
14             });
15 
16             app.Run(async context =>
17             {
18                 await context.Response.WriteAsync("Hello World too!");
19             });
20 
21             // 添加MVC中間件
22             //app.UseMvc();
23         }

啟動調試,再次訪問發現頁面上只有Hello World!字樣。

原因是:Run的這種用法表示注冊的此中間件為管道內的最后一個中間件,由它處理完請求后直接返回。

二、Use方法 

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
 2         {
 3             // 添加日志支持
 4             loggerFactory.AddConsole();
 5             loggerFactory.AddDebug();
 6             
 7             // 添加NLog日志支持
 8             loggerFactory.AddNLog();
 9 
10             // 添加自定義中間件
11             app.Use(async (context, next) =>
12             {
13                 await context.Response.WriteAsync("Hello World!");
14             });
15 
16             // 添加MVC中間件
17             //app.UseMvc();
18         }

啟動調試,訪問頁面同樣顯示Hello World!字樣。我們發現使用Use方法替代Run方法,一樣可以實現同樣的功能。

再次添加一個Use方法,將原來的Use方法內容稍作調整,嘗試實現頁面顯示兩個Hello World!字樣。

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
 2         {
 3             // 添加日志支持
 4             loggerFactory.AddConsole();
 5             loggerFactory.AddDebug();
 6             
 7             // 添加NLog日志支持
 8             loggerFactory.AddNLog();
 9 
10             // 添加自定義中間件
11             app.Use(async (context, next) =>
12             {
13                 await context.Response.WriteAsync("Hello World!");
14                 await next();
15             });
16 
17             app.Use(async (context, next) =>
18             {
19                 await context.Response.WriteAsync("Hello World too!");
20             });
21 
22             // 添加MVC中間件
23             //app.UseMvc();
24         }

啟動調試,訪問頁面

將兩個Use方法換個順序,稍微調整一下內容,再次啟動調試,訪問頁面,發現字樣輸出順序也發生了變化。

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
 2         {
 3             // 添加日志支持
 4             loggerFactory.AddConsole();
 5             loggerFactory.AddDebug();
 6             
 7             // 添加NLog日志支持
 8             loggerFactory.AddNLog(); HelloworldMiddleware.cs 
 9 
10             // 添加自定義中間件
11             app.Use(async (context, next) =>
12             {
13                 await context.Response.WriteAsync("Hello World too!");
14                 await next();
15             });
16 
17             app.Use(async (context, next) =>
18             {
19                 await context.Response.WriteAsync("Hello World!");
20             });
21 
22             // 添加MVC中間件
23             //app.UseMvc();
24         }

從上面的例子可以發現,通過Use方法注冊的中間件,如果不調用next方法,效果等同於Run方法。當調用next方法后,此中間件處理完后將請求傳遞下去,由后續的中間件繼續處理。

當注冊中間件順序不一樣時,處理的順序也不一樣,這一點很重要,當注冊的自定義中間件數量較多時,需要考慮哪些中間件先處理請求,哪些中間件后處理請求。

另外,我們可以將中間件單獨寫成獨立的類,通過UseMiddleware方法同樣可以完成注冊。下面將通過獨立的中間件類重寫上面的演示功能。

新建兩個中間件類: HelloworldMiddleware.cs 、 HelloworldTooMiddleware.cs  

 1 using System.Threading.Tasks;
 2 using Microsoft.AspNetCore.Http;
 3 
 4 namespace WebApiFrame.Core.Middlewares
 5 {
 6     public class HelloworldMiddleware
 7     {
 8         private readonly RequestDelegate _next;
 9 
10         public HelloworldMiddleware(RequestDelegate next){
11             _next = next;
12         }
13 
14         public async Task Invoke(HttpContext context){
15             await context.Response.WriteAsync("Hello World!");
16             await _next(context);
17         }
18     }
19 }
HelloworldMiddleware.cs
 1 using System.Threading.Tasks;
 2 using Microsoft.AspNetCore.Http;
 3 
 4 namespace WebApiFrame.Core.Middlewares
 5 {
 6     public class HelloworldTooMiddleware
 7     {
 8         private readonly RequestDelegate _next;
 9 
10         public HelloworldTooMiddleware(RequestDelegate next){
11             _next = next;
12         }
13 
14         public async Task Invoke(HttpContext context){
15             await context.Response.WriteAsync("Hello World too!");
16         }
17     }
18 }
HelloworldTooMiddleware.cs

修改 Startup.cs 的Configure方法內容

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
 2         {
 3             // 添加日志支持
 4             loggerFactory.AddConsole();
 5             loggerFactory.AddDebug();
 6             
 7             // 添加NLog日志支持
 8             loggerFactory.AddNLog();
 9 
10             // 添加自定義中間件
11             app.UseMiddleware<HelloworldMiddleware>();
12             app.UseMiddleware<HelloworldTooMiddleware>();
13 
14             // 添加MVC中間件
15             //app.UseMvc();
16         }

啟動調試,訪問頁面,可以看到同樣的效果。

三、Map方法

Map方法主要通過請求路徑和其他自定義條件過濾來指定注冊的中間件,看起來更像一個路由。

修改 Startup.cs 的Configure方法內容,增加靜態方法MapTest

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
 2         {
 3             // 添加日志支持
 4             loggerFactory.AddConsole();
 5             loggerFactory.AddDebug();
 6             
 7             // 添加NLog日志支持
 8             loggerFactory.AddNLog();
 9 
10             // 添加自定義中間件
11             app.Map("/test", MapTest);
12 
13             // 添加MVC中間件
14             //app.UseMvc();
15         }
16 
17         private static void MapTest(IApplicationBuilder app){
18             app.Run(async context => {
19                 await context.Response.WriteAsync("Url is " + context.Request.PathBase.ToString());
20             });
21         }

啟動調試,訪問路徑 http://localhost:5000/test ,頁面顯示如下內容

但是訪問其他路徑時,頁面沒有內容顯示。從這個可以看到,Map方法通過類似路由的機制,將特定的Url地址請求引導到固定的方法里,由特定的中間件處理。

另外,Map方法還可以實現多級Url“路由”,其實就是Map方法的嵌套使用

 1             // 添加自定義中間件
 2             app.Map("/level1", lv1App => {
 3                 app.Map("/level1.1", lv11App => {
 4                     // /level1/level1.1
 5 
 6                 });
 7                 
 8                 app.Map("/level1.2", lv12App => {
 9                     // /level1/level1.2
10 
11                 });
12             });

也可以通過MapWhen方法使用自定義條件進行“路由”

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
 2         {
 3             // 添加日志支持
 4             loggerFactory.AddConsole();
 5             loggerFactory.AddDebug();
 6             
 7             // 添加NLog日志支持
 8             loggerFactory.AddNLog();
 9 
10             // 添加自定義中間件
11             app.MapWhen(context =>
12             {
13                 return context.Request.Query.ContainsKey("a");
14             }, MapTest);
15 
16             // 添加MVC中間件
17             //app.UseMvc();
18         }
19 
20         private static void MapTest(IApplicationBuilder app)
21         {
22             app.Run(async context =>
23             {
24                 await context.Response.WriteAsync($"Url is {context.Request.Path.ToString()}{context.Request.QueryString.Value}");
25             });
26 
27         }

啟動調試,訪問路徑 http://localhost:5000/path?a=1&b=2 ,頁面顯示如下內容

只有當請求參數中含有a時,頁面才正常顯示內容。

四、其他內置的中間件

Asp.Net Core框架內置了幾個中間件

 

最后,用自定義中間件實現一個簡單的訪問日志記錄功能,記錄每一次請求的內容和響應時間。

1. 添加日志模型 VisitLog.cs 

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 
 5 namespace WebApiFrame.Models
 6 {
 7     public class VisitLog
 8     {
 9         public string Url { get; set; }
10 
11         public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
12 
13         public string Method { get; set; }
14 
15         public string RequestBody { get; set; }
16 
17         public DateTime ExcuteStartTime { get; set; }
18 
19         public DateTime ExcuteEndTime { get; set; }
20 
21         public override string ToString()
22         {
23             string headers = "[" + string.Join(",", this.Headers.Select(i => "{" + $"\"{i.Key}\":\"{i.Value}\"" + "}")) + "]";
24             return $"Url: {this.Url},\r\nHeaders: {headers},\r\nMethod: {this.Method},\r\nRequestBody: {this.RequestBody},\r\nExcuteStartTime: {this.ExcuteStartTime.ToString("yyyy-MM-dd HH:mm:ss.fff")},\r\nExcuteStartTime: {this.ExcuteEndTime.ToString("yyyy-MM-dd HH:mm:ss.fff")}";
25         }
26     }
27 }

2. 添加訪問日志記錄中間件 VisitLogMiddleware.cs ,同時添加UseVisitLogger擴展方法。

 1 using Microsoft.AspNetCore.Builder;
 2 using Microsoft.AspNetCore.Http;
 3 using Microsoft.Extensions.Logging;
 4 using System;
 5 using System.IO;
 6 using System.Linq;
 7 using System.Threading.Tasks;
 8 using WebApiFrame.Models;
 9 
10 namespace WebApiFrame.Core.Middlewares
11 {
12     public class VisitLogMiddleware
13     {
14         private readonly RequestDelegate _next;
15 
16         private readonly ILogger logger;
17 
18         private VisitLog visitLog;
19 
20         public VisitLogMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
21         {
22             _next = next;
23             logger = loggerFactory.CreateLogger<VisitLogMiddleware>();
24         }
25 
26         public async Task Invoke(HttpContext context)
27         {
28             visitLog = new VisitLog();
29             HttpRequest request = context.Request;
30             visitLog.Url = request.Path.ToString();
31             visitLog.Headers = request.Headers.ToDictionary(k => k.Key, v => string.Join(";", v.Value.ToList()));
32             visitLog.Method = request.Method;
33             visitLog.ExcuteStartTime = DateTime.Now;
34 
35             using (StreamReader reader = new StreamReader(request.Body))
36             {
37                 visitLog.RequestBody = reader.ReadToEnd();
38             }
39 
40             context.Response.OnCompleted(ResponseCompletedCallback, context);
41             await _next(context);
42         }
43 
44         private Task ResponseCompletedCallback(object obj)
45         {
46             visitLog.ExcuteEndTime = DateTime.Now;
47             logger.LogInformation($"VisitLog: {visitLog.ToString()}");
48             return Task.FromResult(0);
49         }
50     }
51 
52     public static class VisitLogMiddlewareExtensions
53     {
54         public static IApplicationBuilder UseVisitLogger(this IApplicationBuilder builder)
55         {
56             return builder.UseMiddleware<VisitLogMiddleware>();
57         }
58     }
59 }

3. 在 Startup.cs 添加中間件支持

 1         public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
 2         {
 3             // 添加日志支持
 4             loggerFactory.AddConsole();
 5             loggerFactory.AddDebug();
 6             
 7             // 添加NLog日志支持
 8             loggerFactory.AddNLog();
 9 
10             // 添加自定義中間件
11             app.UseVisitLogger();
12 
13             app.Run(async context =>
14             {
15                 await context.Response.WriteAsync("Hello World!");
16             });
17 
18 
19             // 添加MVC中間件
20             //app.UseMvc();
21         }

4. 啟動調試,訪問地址 http://localhost:5000/ ,查看調試控制台日志打印信息。

另外,如果你比較細心會發現,在Configure方法里有這樣一句代碼: app.UseMvc(); ,Asp.Net Core Mvc正是通過這個方法借用中間件來擴展實現了MVC框架。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM