您好,登錄后才能下訂單哦!
小編給大家分享一下ASP.NET Core自定義中間件怎么讀取Request.Body與Response.Body的內容,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
背景#
最近在徒手造輪子,編寫一個ASP.NET Core的日志監控器,其中用到了自定義中間件讀取Request.Body和Response.Body的內容,但是編寫過程,并不像想象中的一帆風順,ASP.NET Core針對Request.Body和Response.Body的幾個特殊設計,導致了完成以上功能需要繞一些彎路。
原始代碼#
為了讀取Request.Body和Response.Body的內容,我的實現思路如下:
創建一個LoggerMiddleware的中間件,將它放置在項目中間件管道的頭部。因為根據ASP.NET Core的中間件管道設計,只有第一個中間件才能獲取到原始的請求信息和最終的響應信息。
Request.Body和Response.Body屬性都是Steram類型, 在LoggerMiddleware中間件的InvokeAsync方法中,我們可以分別使用StreamReader讀取Request.Body和Response.Body的內容。
根據以上思路,我編寫了以下代碼。
LoggerMiddleware.cs
public class LoggerMiddleware { private readonly RequestDelegate _next; public LoggerMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd(); Console.WriteLine($"Request Body: {requestContent}"); await _next(context); var responseReader = new StreamReader(context.Response.Body); var responseContent = responseReader.ReadToEnd(); Console.WriteLine($"Response Body: {responseContent}"); } }
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseMiddleware<LoggerMiddleware>(); app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseHttpsRedirection(); app.UseMvc(); }
問題1:Response.Body的Stream不可讀#
這里為了測試我創建了一個默認的ASP.NET Core WebApi項目。當運行程序,使用GET方式調用/api/values之后,控制臺會返回第一個需要處理的錯誤。
System.ArgumentException: Stream was not readable.
即ASP.NET Core默認創建的Response.Body屬性是不可讀的。
這一點我們可以通過打斷點看到Response.Body屬性的CanRead值是false。
這就很糟糕了,ASP.NET Core默認并不想讓我們在中間件中直接讀取Response.Body中的信息。
這里看似的無解,但是我們可以轉換一下思路,既然ASP.NET Core默認將Response.Body是不可讀的,那么我們就使用一個可讀可寫的Stream對象將其替換掉。這樣當所有中間件都依次執行完之后,我們就可以讀取Response.Body的內容了。
public async Task InvokeAsync(HttpContext context) { var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd(); Console.WriteLine($"Request Body: {requestContent}"); using (var ms = new MemoryStream()) { context.Response.Body = ms; await _next(context); context.Response.Body.Position = 0; var responseReader = new StreamReader(context.Response.Body); var responseContent = responseReader.ReadToEnd(); Console.WriteLine($"Response Body: {responseContent}"); context.Response.Body.Position = 0; } }
注意:
讀取Response.Body的時候,需要設置Position = 0, 這樣是為了重置指針,如果不這樣做的話,會導致讀取的流不正確。
這里千萬不要用using包裹StreamReader, 因為StreamReader會在讀取完Stream內容之后,將Stream關閉,導致后續由于Stream關閉,而不能再次讀取Stream中的內容。如果必須使用,請使用StreamReader的以下重載,將leaveOpen參數設置為true, 確保StreamReader對象被銷毀的時候不會自動關閉讀取的Stream.
public StreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize, bool leaveOpen);
重新啟動程序,請求/api/values, 我們就得到的正確的結果。
進一步完善代碼#
以上代碼實現,看似已經能夠讀取Response.Body的內容了,但是其實還是有問題的。
回想一下,我們做出以上方案的前提是,當前LoggerMiddleware中間件必須位于中間件管道的頭部。
如果不能保證這個約定, 就會出現問題,因為我們在LoggerMiddleware中間件中將Response.Body屬性指向了一個新的可讀可寫的Stream對象。如果LoggerMiddleware中間件之前的某個中間件中設置過Response.Body, 就會導致這部分設置丟失。
因此正確的設置方式應該是這樣的:
public async Task InvokeAsync(HttpContext context) { var originalResponseStream = context.Response.Body; var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd(); Console.WriteLine($"Request Body: {requestContent}"); using (var ms = new MemoryStream()) { context.Response.Body = ms; await _next(context); ms.Position = 0; var responseReader = new StreamReader(ms); var responseContent = responseReader.ReadToEnd(); Console.WriteLine($"Response Body: {responseContent}"); ms.Position = 0; await ms.CopyToAsync(originalResponseStream); context.Response.Body = originalResponseStream; } }
代碼解釋:
這里當進入LoggerMiddleware中間件時,我們將之前中間件操作完成之后的Response.Body對象對應的原始Stream, 保存在一個臨時變量中
當LoggerMiddelware中間件的任務完成之后,我們需要將后續產生的Response.Body流追加到原始Stream中,然后將Response.Body對象重置為這個新的Stream。
至此Repsonse.Body的問題都解決,下面我們再來看一下Request.Body的問題。
問題2:Request.Body的內容可以正確的顯示,但是后續的ModelBinding都失敗了#
下面我們來請求POST /api/values, Request.Body里面的內容是字符串"123123"
服務器端返回了400錯誤, 錯誤信息
A non-empty request body is required.
這里就很奇怪,為啥請求體是空呢?我們回到中間件部分代碼,這里我們在讀取完Request.Body中的Stream之后,沒有將Stream的指針重置,當前指針已經是Stream的尾部,所以后續ModelBinding的時候,讀取不到Stream的內容了。
public async Task InvokeAsync(HttpContext context) { ... var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd(); Console.WriteLine($"Request Body: {requestContent}"); ... }
于是,這里我們需要采取和Response.Body相同的處理方式,在讀取完Request.Body之后,我們需要將Request.Body的Stream指針重置
public async Task InvokeAsync(HttpContext context) { ... var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd(); Console.WriteLine($"Request Body: {requestContent}"); context.Request.Body.Position = 0; ... }
你一定覺著至此問題就解決了,不過ASP.NET Core和你又開了一個玩笑。
當你重新請求POST /api/values之后,你會得到以下結果。
錯誤原因:
System.NotSupportedException: Specified method is not supported.
翻譯過來就是指定方法不支持。到底不支持啥呢?在代碼上打上斷點,你會發現Request.Body的CanSeek屬性是false, 即Request.Body的Stream, 你是不能隨便移動指針的,只能按順序讀取一次,默認不支持反復讀取。
那么如何解決這個問題呢?
你可以在使用Request對象中的EnableRewind或者EnableBuffering。 這2個方法的作用都是在內存中創建緩沖區存放Request.Body的內容,從而允許反復讀取Request.Body的Stream。
說明: 其實EnableBuffering方法內部就只直接調用的EnableRewind方法。
下面我們修改代碼
public async Task InvokeAsync(HttpContext context) { context.Request.EnableBuffering(); var requestReader = new StreamReader(context.Request.Body); var requestContent = requestReader.ReadToEnd(); Console.WriteLine($"Request Body: {requestContent}"); context.Request.Body.Position = 0; using (var ms = new MemoryStream()) { context.Response.Body = ms; await _next(context); ms.Position = 0; var responseReader = new StreamReader(ms); var responseContent = responseReader.ReadToEnd(); Console.WriteLine($"Response Body: {responseContent}"); ms.Position = 0; } }
再次請求POST /api/values, api請求被正確的處理了。
以上是“ASP.NET Core自定義中間件怎么讀取Request.Body與Response.Body的內容”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。