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

LamondLu發表於2019-05-31

文章名稱: 如何在ASP.NET Core自定義中介軟體讀取Request.Body和Response.Body的內容?
作者: Lamond Lu
地址: https://www.cnblogs.com/lwqlun/p/10954936.html
原始碼: https://github.com/lamondlu/webapi-logger

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

背景

最近在徒手造輪子,編寫一個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的中介軟體管道設計,只有第一個中介軟體才能獲取到原始的請求資訊和最終的響應資訊。
    如何在ASP.NET Core自定義中介軟體中讀取Request.Body和Response.Body的內容?

  • 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, 我們就得到的正確的結果。
如何在ASP.NET Core自定義中介軟體中讀取Request.Body和Response.Body的內容?

進一步完善程式碼

以上程式碼實現,看似已經能夠讀取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"

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

伺服器端返回了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之後,你會得到以下結果。

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

錯誤原因:

System.NotSupportedException: Specified method is not supported.

翻譯過來就是指定方法不支援。到底不支援啥呢?在程式碼上打上斷點,你會發現Request.Body的CanSeek屬性是false, 即Request.Body的Stream, 你是不能隨便移動指標的,只能按順序讀取一次,預設不支援反覆讀取。

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

那麼如何解決這個問題呢?

你可以在使用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請求被正確的處理了。

相關文章