如何在ASP.NET Core自定義中間件中讀取Request.Body和Response.Body的內容?

文章名稱: 如何在ASP.NET Core自定義中間件讀取Request.Body和Response.Body的內容? 做者: Lamond Lu 地址: http://www.javashuo.com/article/p-tpmbwnuf-bq.html 源代碼: https://github.com/lamondlu/webapi-loggerhtml

背景

最近在徒手造輪子,編寫一個ASP.NET Core的日誌監控器,其中用到了自定義中間件讀取Request.Body和Response.Body的內容,可是編寫過程,並不像想象中的一路順風,ASP.NET Core針對Request.Body和Response.Body的幾個特殊設計,致使了完成以上功能須要繞一些彎路。git

原始代碼

爲了讀取Request.Body和Response.Body的內容,個人實現思路以下:github

  • 建立一個LoggerMiddleware的中間件,將它放置在項目中間件管道的頭部。由於根據ASP.NET Core的中間件管道設計,只有第一個中間件才能獲取到原始的請求信息和最終的響應信息。 web

  • Request.Body和Response.Body屬性都是Steram類型, 在LoggerMiddleware中間件的InvokeAsync方法中,咱們能夠分別使用StreamReader讀取Request.Body和Response.Body的內容。c#

根據以上思路,我編寫了如下代碼。api

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.csapp

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以後,控制檯會返回第一個須要處理的錯誤。async

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請求被正確的處理了。

原文出處:https://www.cnblogs.com/lwqlun/p/10954936.html

相關文章
相關標籤/搜索