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

 

 

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

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

第一部分:管道模型

1. Asp.Net管道

在以前的Asp.Net裏,主要的管道模型流程以下圖所示:app

請求進入Asp.Net工做進程後,由進程建立HttpWorkRequest對象,封裝這次請求有關的全部信息,而後進入HttpRuntime類進行進一步處理。HttpRuntime經過請求信息建立HttpContext上下文對象,此對象將貫穿整個管道,直到響應結束。同時建立或從應用程序池裏初始化一個HttpApplication對象,由此對象開始處理以前註冊的多個HttpModule。以後調用HandlerFactory建立Handler處理程序,最終處理這次請求內容,生成響應返回。框架

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

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

複製代碼
 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 }
複製代碼
複製代碼
 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 }
複製代碼

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

複製代碼
 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 ,能夠看到頁面內容。ui

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

二、Asp.Net Core管道

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

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

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

中間件就像一層一層的「濾網」,過濾全部的請求和相應。這一設計很是適用於「請求-響應」這樣的場景——消息從管道頭流入最後反向流出。

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

 

第二部分、Middleware

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

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

1、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的這種用法表示註冊的此中間件爲管道內的最後一箇中間件,由它處理完請求後直接返回。

2、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 }
複製代碼
複製代碼
 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 }
複製代碼

修改 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 }
複製代碼

啓動調試,訪問頁面,能夠看到一樣的效果。

3、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時,頁面才正常顯示內容。

4、其餘內置的中間件

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框架。 

相關文章
相關標籤/搜索